forked from nerfstudio-project/viser
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor scene pointer events + support rectangular selection (nerfst…
…udio-project#157) * Initial commit for 3D scribble, WIP * Add Viewer2DCanvas, use that to draw cursor feedback! * Remove scribble support, smoothen out box select, add example * Correctly update canvas size on resize * Refactor ScenePointerMessage, send 2D coordinates for box * Update example, walk through OpenCV/NDC coordinates * Rename SceneClickEnableMessage->ScenePointerEnableMessage * ruff, mypy * Put camera in a reasonable initial location * Add event type to on_scene_pointer * Rename example file from scene_click->scene_pointer * mypy, ruff * Add backwards compatibility * ruff, mypy * ruff, mypy * Rename click->pointer * Use resizeobserver for element size tracking * Use OpenCV image coords (normalized) * mypy * Remove irrelevant file * mypy * ruff, pyright * mypy * Nits * Try to fix mypy * Rename box to rect-select * Listen to only one scenepointerevent at a given time. * mypy, ruff * ruff * Track events for clients! * Clear pre-existing callbacks for both server/clients, include warnings. * ruff * Code nit * Nits * oops * Move pointer cleanup logic --------- Co-authored-by: Brent Yi <[email protected]>
- Loading branch information
1 parent
752dcd7
commit 35331a8
Showing
13 changed files
with
753 additions
and
278 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
.. Comment: this file is automatically generated by `update_example_docs.py`. | ||
It should not be modified manually. | ||
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``. | ||
|
||
|
||
|
||
.. code-block:: python | ||
:linenos: | ||
import time | ||
from pathlib import Path | ||
from typing import List, cast | ||
import numpy as onp | ||
import trimesh | ||
import trimesh.creation | ||
import trimesh.ray | ||
import viser | ||
import viser.transforms as tf | ||
server = viser.ViserServer() | ||
server.configure_theme(brand_color=(130, 0, 150)) | ||
server.set_up_direction("+y") | ||
mesh = cast( | ||
trimesh.Trimesh, trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj")) | ||
) | ||
mesh.apply_scale(0.05) | ||
mesh_handle = server.add_mesh_trimesh( | ||
name="/mesh", | ||
mesh=mesh, | ||
position=(0.0, 0.0, 0.0), | ||
) | ||
hit_pos_handles: List[viser.GlbHandle] = [] | ||
# Buttons + callbacks will operate on a per-client basis, but will modify the global scene! :) | ||
@server.on_client_connect | ||
def _(client: viser.ClientHandle) -> None: | ||
# Set up the camera -- this gives a nice view of the full mesh. | ||
client.camera.position = onp.array([0.0, 0.0, -10.0]) | ||
client.camera.wxyz = onp.array([0.0, 0.0, 0.0, 1.0]) | ||
# Tests "click" scenepointerevent. | ||
click_button_handle = client.add_gui_button("Add sphere", icon=viser.Icon.POINTER) | ||
@click_button_handle.on_click | ||
def _(_): | ||
click_button_handle.disabled = True | ||
@client.on_scene_pointer(event_type="click") | ||
def _(event: viser.ScenePointerEvent) -> None: | ||
# Check for intersection with the mesh, using trimesh's ray-mesh intersection. | ||
# Note that mesh is in the mesh frame, so we need to transform the ray. | ||
R_world_mesh = tf.SO3(mesh_handle.wxyz) | ||
R_mesh_world = R_world_mesh.inverse() | ||
origin = (R_mesh_world @ onp.array(event.ray_origin)).reshape(1, 3) | ||
direction = (R_mesh_world @ onp.array(event.ray_direction)).reshape(1, 3) | ||
intersector = trimesh.ray.ray_triangle.RayMeshIntersector(mesh) | ||
hit_pos, _, _ = intersector.intersects_location(origin, direction) | ||
if len(hit_pos) == 0: | ||
return | ||
# Get the first hit position (based on distance from the ray origin). | ||
hit_pos = min(hit_pos, key=lambda x: onp.linalg.norm(x - origin)) | ||
# Create a sphere at the hit location. | ||
hit_pos_mesh = trimesh.creation.icosphere(radius=0.1) | ||
hit_pos_mesh.vertices += R_world_mesh @ hit_pos | ||
hit_pos_mesh.visual.vertex_colors = (0.5, 0.0, 0.7, 1.0) # type: ignore | ||
hit_pos_handle = server.add_mesh_trimesh( | ||
name=f"/hit_pos_{len(hit_pos_handles)}", mesh=hit_pos_mesh | ||
) | ||
hit_pos_handles.append(hit_pos_handle) | ||
@client.on_scene_pointer_done | ||
def _(): | ||
click_button_handle.disabled = False | ||
client.remove_scene_pointer_callback() | ||
# Tests "rect-select" scenepointerevent. | ||
paint_button_handle = client.add_gui_button("Paint mesh", icon=viser.Icon.PAINT) | ||
@paint_button_handle.on_click | ||
def _(_): | ||
paint_button_handle.disabled = True | ||
@client.on_scene_pointer(event_type="rect-select") | ||
def _(message: viser.ScenePointerEvent) -> None: | ||
global mesh_handle | ||
camera = message.client.camera | ||
# Put the mesh in the camera frame. | ||
R_world_mesh = tf.SO3(mesh_handle.wxyz) | ||
R_mesh_world = R_world_mesh.inverse() | ||
R_camera_world = tf.SE3.from_rotation_and_translation( | ||
tf.SO3(camera.wxyz), camera.position | ||
).inverse() | ||
vertices = mesh.vertices | ||
vertices = (R_mesh_world.as_matrix() @ vertices.T).T | ||
vertices = ( | ||
R_camera_world.as_matrix() | ||
@ onp.hstack([vertices, onp.ones((vertices.shape[0], 1))]).T | ||
).T[:, :3] | ||
# Get the camera intrinsics, and project the vertices onto the image plane. | ||
fov, aspect = camera.fov, camera.aspect | ||
vertices_proj = vertices[:, :2] / vertices[:, 2].reshape(-1, 1) | ||
vertices_proj /= onp.tan(fov / 2) | ||
vertices_proj[:, 0] /= aspect | ||
# Move the origin to the upper-left corner, and scale to [0, 1]. | ||
# ... make sure to match the OpenCV's image coordinates! | ||
vertices_proj = (1 + vertices_proj) / 2 | ||
# Select the vertices that lie inside the 2D selected box, once projected. | ||
mask = ( | ||
(vertices_proj > onp.array(message.screen_pos[0])) | ||
& (vertices_proj < onp.array(message.screen_pos[1])) | ||
).all(axis=1)[..., None] | ||
# Update the mesh color based on whether the vertices are inside the box | ||
mesh.visual.vertex_colors = onp.where( # type: ignore | ||
mask, (0.5, 0.0, 0.7, 1.0), (0.9, 0.9, 0.9, 1.0) | ||
) | ||
mesh_handle = server.add_mesh_trimesh( | ||
name="/mesh", | ||
mesh=mesh, | ||
position=(0.0, 0.0, 0.0), | ||
) | ||
@client.on_scene_pointer_done | ||
def _(): | ||
paint_button_handle.disabled = False | ||
client.remove_scene_pointer_callback() | ||
# Button to clear spheres. | ||
clear_button_handle = client.add_gui_button("Clear scene", icon=viser.Icon.X) | ||
@clear_button_handle.on_click | ||
def _(_): | ||
"""Reset the mesh color and remove all click-generated spheres.""" | ||
global mesh_handle | ||
for handle in hit_pos_handles: | ||
handle.remove() | ||
hit_pos_handles.clear() | ||
mesh.visual.vertex_colors = (0.9, 0.9, 0.9, 1.0) # type: ignore | ||
mesh_handle = server.add_mesh_trimesh( | ||
name="/mesh", | ||
mesh=mesh, | ||
position=(0.0, 0.0, 0.0), | ||
) | ||
while True: | ||
time.sleep(10.0) |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.