Skip to content

Commit 82ec787

Browse files
authored
feat(viser): add viser viewer (#628)
Signed-off-by: Hirokazu Ishida (SB Intuitions) <[email protected]>
1 parent c281c58 commit 82ec787

File tree

4 files changed

+151
-3
lines changed

4 files changed

+151
-3
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ scipy<=1.2.3;python_version<"3.0"
2626
scipy>=1.6.3;python_version>="3.8"
2727
six
2828
trimesh>=3.9.0,!=3.23.0
29+
viser

skrobot/model/primitives.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ def __init__(self,
101101

102102
super(Axis, self).__init__(pos=pos, rot=rot, name=name,
103103
visual_mesh=visual_mesh)
104+
self._axis_length = axis_length
105+
self._axis_radius = axis_radius
106+
107+
@property
108+
def axis_length(self):
109+
return self._axis_length
110+
111+
@property
112+
def axis_radius(self):
113+
return self._axis_radius
104114

105115
@classmethod
106116
def from_coords(cls, coords, **kwargs):
@@ -136,15 +146,18 @@ def __init__(self, extents, vertex_colors=None, face_colors=None,
136146
super(Box, self).__init__(pos=pos, rot=rot, name=name,
137147
collision_mesh=mesh,
138148
visual_mesh=mesh)
139-
self.extents = extents
140-
self._extents = extents # for backward compatibility
149+
self._extents = extents
141150
if with_sdf:
142151
sdf = BoxSDF(extents)
143152
self.assoc(sdf, relative_coords="local")
144153
self._sdf = sdf
145154
else:
146155
self._sdf = None
147156

157+
@property
158+
def extents(self):
159+
return self._extents
160+
148161

149162
class CameraMarker(Link):
150163

@@ -244,14 +257,18 @@ def __init__(self, radius, subdivisions=3, color=None,
244257
collision_mesh=mesh,
245258
visual_mesh=mesh)
246259

247-
self.radius = radius
260+
self._radius = radius
248261
if with_sdf:
249262
sdf = SphereSDF(radius)
250263
self.assoc(sdf, relative_coords="local")
251264
self._sdf = sdf
252265
else:
253266
self._sdf = None
254267

268+
@property
269+
def radius(self):
270+
return self._radius
271+
255272

256273
class Annulus(Link):
257274

skrobot/viewers/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,9 @@ class PyrenderViewer(DummyViewer):
6666
except ImportError:
6767
class JupyterNotebookViewer(DummyViewer):
6868
pass
69+
70+
try:
71+
from ._viser import ViserVisualizer
72+
except ImportError:
73+
class ViserVisualizer(DummyViewer):
74+
pass

skrobot/viewers/_viser.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from typing import Union
2+
import webbrowser
3+
4+
import numpy as np
5+
import trimesh
6+
import viser
7+
8+
from skrobot.coordinates.math import matrix2quaternion
9+
from skrobot.model.link import Link
10+
from skrobot.model.primitives import Axis
11+
from skrobot.model.primitives import LineString
12+
from skrobot.model.primitives import PointCloudLink
13+
from skrobot.model.primitives import Sphere
14+
from skrobot.model.robot_model import CascadedLink
15+
16+
17+
class ViserVisualizer:
18+
def __init__(self, draw_grid: bool = True):
19+
self._server = viser.ViserServer()
20+
self._linkid_to_handle = dict()
21+
self._linkid_to_link = dict()
22+
23+
def draw_grid(self, width: float = 20.0, height: float = -0.001):
24+
self._server.scene.add_grid(
25+
"/grid",
26+
width=20.0,
27+
height=20.0,
28+
position=np.array([0.0, 0.0, -0.01]),
29+
)
30+
31+
def _add_link(self, link: Link):
32+
assert isinstance(link, Link)
33+
link_id = str(id(link))
34+
if link_id in self._linkid_to_handle:
35+
return
36+
37+
handle = None
38+
if isinstance(link, Sphere):
39+
# Although sphere can be treated as trimesh, naively rendering
40+
# it requires high cost. Therefore, we use an analytic sphere.
41+
color = link.visual_mesh.visual.face_colors[0, :3]
42+
alpha = link.visual_mesh.visual.face_colors[0, 3]
43+
if alpha > 1.0:
44+
alpha = alpha / 255.0
45+
handle = self._server.scene.add_icosphere(
46+
link.name,
47+
radius=link.radius,
48+
position=link.worldpos(),
49+
color=color,
50+
opacity=alpha)
51+
elif isinstance(link, Axis):
52+
handle = self._server.scene.add_frame(
53+
link.name,
54+
axes_length=link.axis_length,
55+
axes_radius=link.axis_radius,
56+
wxyz=matrix2quaternion(link.worldrot()),
57+
position=link.worldpos(),
58+
)
59+
elif isinstance(link, PointCloudLink):
60+
mesh = link.visual_mesh
61+
assert isinstance(mesh, trimesh.PointCloud)
62+
if len(mesh.colors) > 0:
63+
colors = mesh.colors[:, :3]
64+
else:
65+
colors = np.zeros(3)
66+
self._server.scene.add_point_cloud(
67+
link.name,
68+
points=mesh.vertices,
69+
colors=colors,
70+
point_size=0.002, # TODO(HiroIshida): configurable
71+
)
72+
elif isinstance(link, LineString):
73+
raise NotImplementedError("not implemented yet")
74+
else:
75+
mesh = link.concatenated_visual_mesh
76+
if mesh is not None:
77+
handle = self._server.scene.add_mesh_trimesh(
78+
link.name,
79+
mesh=mesh,
80+
wxyz=matrix2quaternion(link.worldrot()),
81+
position=link.worldpos(),
82+
)
83+
84+
if handle is not None:
85+
self._linkid_to_link[link_id] = link
86+
self._linkid_to_handle[link_id] = handle
87+
88+
def add(self, geometry: Union[Link, CascadedLink]):
89+
if isinstance(geometry, Link):
90+
self._add_link(geometry)
91+
elif isinstance(geometry, CascadedLink):
92+
for link in geometry.link_list:
93+
self._add_link(link)
94+
else:
95+
raise TypeError("geometry must be Link or CascadedLink")
96+
97+
def show(self):
98+
host = self._server.get_host()
99+
port = self._server.get_port()
100+
url = f"http://{host}:{port}"
101+
webbrowser.open(url)
102+
103+
def redraw(self):
104+
for link_id, handle in self._linkid_to_handle.items():
105+
link = self._linkid_to_link[link_id]
106+
handle.position = link.worldpos()
107+
handle.wxyz = matrix2quaternion(link.worldrot())
108+
109+
def delete(self, geometry: Union[Link, CascadedLink]):
110+
if isinstance(geometry, Link):
111+
links = [geometry]
112+
elif isinstance(geometry, CascadedLink):
113+
links = geometry.link_list
114+
else:
115+
raise TypeError("geometry must be Link or CascadedLink")
116+
117+
for link in links:
118+
link_id = str(id(link))
119+
if link_id not in self._linkid_to_handle:
120+
continue
121+
handle = self._linkid_to_handle[link_id]
122+
handle.remove()
123+
self._linkid_to_link.pop(link_id)
124+
self._linkid_to_handle.pop(link_id)

0 commit comments

Comments
 (0)