Skip to content

Commit d297b9a

Browse files
authored
[PyrenderViewer] Add v option to switch visual mesh and collision mesh (#579)
1 parent 50d0d89 commit d297b9a

File tree

3 files changed

+255
-8
lines changed

3 files changed

+255
-8
lines changed
161 KB
Loading

docs/source/reference/viewers.rst

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,156 @@ PyrenderViewer
6262
--------------
6363

6464
**Description:**
65-
The ``PyrenderViewer`` utilizes the Pyrender library for advanced 3D rendering, ideal for creating realistic visual simulations. This viewer is particularly suited for complex rendering tasks in robotics, including detailed lighting and shading effects.
65+
The ``PyrenderViewer`` utilizes the Pyrender library for advanced 3D rendering, ideal for creating realistic visual simulations. This viewer is implemented as a Singleton to ensure only one instance exists throughout the program. It's particularly suited for complex rendering tasks in robotics, including detailed lighting, shading effects, and collision visualization.
6666

6767
**Key Functionalities:**
6868

6969
- **Initialization and Configuration:**
70-
The viewer is initialized with specified resolution and rendering flags, creating a scene managed by Pyrender. It supports high-quality rendering features like raymond lighting.
70+
The viewer is initialized with specified resolution, update interval, and rendering flags. Key parameters include:
71+
72+
- ``resolution``: Window size (default: ``(640, 480)``)
73+
- ``update_interval``: Update frequency in seconds (default: ``1.0``)
74+
- ``enable_collision_toggle``: Enable collision/visual mesh switching (default: ``True``)
75+
- ``title``: Window title (default: ``'scikit-robot PyrenderViewer'``)
7176

7277
- **Rendering Control:**
73-
Handles real-time scene updates triggered by user interactions such as mouse events and keyboard inputs, ensuring the scene remains interactive and up-to-date.
78+
Handles real-time scene updates triggered by user interactions. The viewer automatically manages OpenGL compatibility with fallback support from OpenGL 4.1 → 4.0 → 3.3, ensuring robust operation across different systems including WSL2.
7479

7580
- **Scene Management:**
76-
Similar to ``TrimeshSceneViewer``, it allows for the addition and removal of visual meshes linked to robotic models, supporting dynamic updates to the scene as robotic configurations change.
81+
Supports dynamic addition and removal of visual and collision meshes linked to robotic models. The viewer maintains real-time synchronization with robot configurations through the ``redraw()`` method.
7782

7883
- **Camera Management:**
79-
Offers detailed camera setup options, including angle adjustments, distance settings, center positioning, and field of view configuration, providing flexibility in viewing angles for complex scenes.
84+
Offers detailed camera setup options through the ``set_camera()`` method:
85+
86+
- Angle-based positioning with Euler angles
87+
- Distance and center point configuration
88+
- Field of view (FOV) adjustment
89+
- Direct Coordinates object support for precise camera placement
90+
91+
- **Collision/Visual Mesh Toggle:**
92+
When ``enable_collision_toggle=True``, press the ``v`` key to switch between:
93+
94+
- **Visual meshes**: Default appearance meshes for rendering (left in figure below)
95+
- **Collision meshes**: Simplified meshes used for collision detection (displayed in orange/transparent, right in figure below)
96+
97+
.. figure:: ../_static/visual-collision-comparison.jpg
98+
:width: 100%
99+
:align: center
100+
:alt: Visual mesh (left) vs Collision mesh (right) comparison
101+
102+
**Visual mesh (left) vs Collision mesh (right).** The visual mesh shows the detailed appearance of the robot with textured wheels. The collision mesh on the right uses simplified cylinder representations for the wheels, which are computationally more efficient for collision detection algorithms.
103+
104+
- **360-Degree Image Capture:**
105+
The ``capture_360_images()`` method enables automated scene capture from multiple angles:
106+
107+
- Configurable number of frames and camera elevation
108+
- Automatic GIF animation generation
109+
- Transparent background support
110+
- Custom lighting configuration options
111+
112+
113+
**Keyboard Controls:**
114+
115+
The PyrenderViewer provides extensive keyboard controls for interactive manipulation:
116+
117+
.. list-table:: Keyboard Controls
118+
:header-rows: 1
119+
:widths: 10 90
120+
121+
* - Key
122+
- Function
123+
* - ``a``
124+
- Toggle rotational animation mode
125+
* - ``c``
126+
- Toggle backface culling
127+
* - ``f``
128+
- Toggle fullscreen mode
129+
* - ``h``
130+
- Toggle shadow rendering (may impact performance)
131+
* - ``i``
132+
- Cycle through axis display modes (none → world → mesh → all)
133+
* - ``l``
134+
- Cycle lighting modes (scene → Raymond → direct)
135+
* - ``m``
136+
- Toggle face normal visualization
137+
* - ``n``
138+
- Toggle vertex normal visualization
139+
* - ``o``
140+
- Toggle orthographic camera mode
141+
* - ``q``
142+
- Quit the viewer
143+
* - ``r``
144+
- Start/stop GIF recording (opens file dialog on stop)
145+
* - ``s``
146+
- Save current view as image (opens file dialog)
147+
* - ``v``
148+
- **Toggle between visual and collision meshes** (if enabled)
149+
* - ``w``
150+
- Cycle wireframe modes
151+
* - ``z``
152+
- Reset camera to default view
153+
154+
**Mouse Controls:**
155+
156+
- **Left-click + drag**: Rotate camera around scene center
157+
- **Ctrl + Left-click + drag**: Rotate camera around viewing axis
158+
- **Shift + Left-click + drag** or **Middle-click + drag**: Pan camera
159+
- **Right-click + drag** or **Scroll wheel**: Zoom in/out
160+
161+
**Example Usage:**
162+
163+
Basic viewer initialization and robot display:
164+
165+
.. code-block:: python
166+
167+
from skrobot.viewers import PyrenderViewer
168+
from skrobot.models import PR2
169+
170+
# Create viewer instance (Singleton pattern ensures only one instance)
171+
viewer = PyrenderViewer(resolution=(800, 600), update_interval=1.0/30)
172+
173+
# Load and add robot model
174+
robot = PR2()
175+
viewer.add(robot)
176+
177+
# Show the viewer window
178+
viewer.show()
179+
180+
# Update robot pose and redraw
181+
robot.reset_manip_pose()
182+
viewer.redraw()
183+
184+
Collision/Visual mesh toggle example:
185+
186+
.. code-block:: python
187+
188+
# Enable collision toggle functionality
189+
viewer = PyrenderViewer(enable_collision_toggle=True)
190+
191+
# Add robot to viewer
192+
viewer.add(robot)
193+
viewer.show()
194+
195+
# Press 'v' key in the viewer to toggle between visual and collision meshes
196+
# Collision meshes will appear in orange/transparent color
197+
198+
# The visual mesh displays the full detailed geometry with textures
199+
# while collision mesh shows simplified shapes (e.g., cylinders for wheels)
200+
# optimized for physics calculations
201+
202+
360-degree image capture example:
203+
204+
.. code-block:: python
80205
206+
# Capture 360-degree rotation images
207+
viewer.capture_360_images(
208+
output_dir="./robot_360",
209+
num_frames=36, # One image every 10 degrees
210+
camera_elevation=45, # Camera elevation angle
211+
create_gif=True, # Generate animated GIF
212+
gif_duration=100, # 100ms between frames
213+
transparent_background=True # Render with transparent background
214+
)
81215
82216
.. caution::
83217

@@ -88,6 +222,7 @@ PyrenderViewer
88222
.. code-block:: python
89223
90224
viewer = skrobot.viewers.TrimeshSceneViewer(resolution=(640, 480), update_interval=1.0/30) # Set update interval for 30 Hz
225+
viewer = skrobot.viewers.PyrenderViewer(resolution=(640, 480), update_interval=1.0/30) # Same for PyrenderViewer
91226
92227
93228
Color Management

skrobot/viewers/_pyrender.py

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,26 @@ class PyrenderViewer(pyrender.Viewer):
7676
1.0 seconds.
7777
title : str, optional
7878
The title of the viewer window. Default is 'scikit-robot PyrenderViewer'.
79+
enable_collision_toggle : bool, optional
80+
Enable collision/visual mesh toggle functionality with 'v' key.
81+
Default is True.
7982
8083
Notes
8184
-----
8285
Since this is a singleton, the __init__ method might be called
8386
multiple times, but only one instance is actually used.
87+
88+
Keyboard Controls
89+
-----------------
90+
v : Toggle between visual and collision meshes (if enable_collision_toggle=True)
91+
Collision meshes are displayed in orange/transparent color
8492
"""
8593

8694
# Class variable to hold the single instance of the class.
8795
_instance = None
8896

8997
def __init__(self, resolution=None, update_interval=1.0,
90-
render_flags=None, title=None):
98+
render_flags=None, title=None, enable_collision_toggle=True):
9199
if getattr(self, '_initialized', False):
92100
return
93101
if resolution is None:
@@ -96,6 +104,12 @@ def __init__(self, resolution=None, update_interval=1.0,
96104
self.thread = None
97105
self._visual_mesh_map = collections.OrderedDict()
98106

107+
# Collision toggle functionality
108+
self.enable_collision_toggle = enable_collision_toggle
109+
if self.enable_collision_toggle:
110+
self._stored_links = []
111+
self.show_collision = False
112+
99113
self._redraw = True
100114

101115
refresh_rate = 1.0 / update_interval
@@ -228,9 +242,26 @@ def on_mouse_scroll(self, *args, **kwargs):
228242
self._redraw = True
229243
return super(PyrenderViewer, self).on_mouse_scroll(*args, **kwargs)
230244

231-
def on_key_press(self, *args, **kwargs):
245+
def on_key_press(self, symbol, modifiers, *args, **kwargs):
246+
"""Handle key press events with collision toggle support."""
247+
# Handle 'v' key for collision toggle if enabled
248+
if self.enable_collision_toggle:
249+
from pyglet.window import key
250+
if symbol == key.V:
251+
# Toggle display mode
252+
self.show_collision = not self.show_collision
253+
254+
# Rebuild scene with current mesh type
255+
self._rebuild_scene_for_toggle()
256+
257+
mode_text = "Collision" if self.show_collision else "Visual"
258+
print(f"Switched to {mode_text.lower()} mesh display")
259+
260+
self._redraw = True
261+
return True
262+
232263
self._redraw = True
233-
return super(PyrenderViewer, self).on_key_press(*args, **kwargs)
264+
return super(PyrenderViewer, self).on_key_press(symbol, modifiers, *args, **kwargs)
234265

235266
def on_resize(self, *args, **kwargs):
236267
self._redraw = True
@@ -283,6 +314,12 @@ def add(self, geometry):
283314
else:
284315
raise TypeError('geometry must be Link or CascadedLink')
285316

317+
# Store links for collision toggle if enabled
318+
if self.enable_collision_toggle:
319+
for link in links:
320+
if link not in self._stored_links:
321+
self._stored_links.append(link)
322+
286323
for link in links:
287324
self._add_link(link)
288325

@@ -568,3 +605,78 @@ def _calculate_camera_distance(self, distance_margin=1.2):
568605
bounds = self.scene.bounds
569606
bbox_diagonal = np.linalg.norm(bounds[1] - bounds[0])
570607
return bbox_diagonal * distance_margin
608+
609+
def _rebuild_scene_for_toggle(self):
610+
"""Completely rebuild the scene with current mesh type for toggle functionality."""
611+
if not self.enable_collision_toggle:
612+
return
613+
614+
with self._render_lock:
615+
# Clear all mesh nodes but preserve camera and lights
616+
mesh_nodes_to_remove = []
617+
618+
for node in list(self.scene.nodes):
619+
if node.camera is None and node.light is None and node.mesh is not None:
620+
mesh_nodes_to_remove.append(node)
621+
622+
# Remove mesh nodes
623+
for node in mesh_nodes_to_remove:
624+
self.scene.remove_node(node)
625+
626+
# Clear visual mesh map
627+
self._visual_mesh_map.clear()
628+
629+
# Add meshes for current mode
630+
for link in self._stored_links:
631+
self._add_single_link_mesh_for_toggle(link)
632+
633+
def _add_single_link_mesh_for_toggle(self, link):
634+
"""Add a single mesh (visual or collision) for a link during toggle."""
635+
if not isinstance(link, model_module.Link):
636+
return
637+
638+
link_id = str(id(link))
639+
transform = link.worldcoords().T()
640+
641+
# Choose mesh based on current mode
642+
if self.show_collision:
643+
mesh = link.collision_mesh
644+
# Process collision mesh with orange coloring
645+
if mesh is not None:
646+
if isinstance(mesh, list):
647+
colored_meshes = []
648+
for m in mesh:
649+
colored_mesh = m.copy()
650+
colored_mesh.visual.face_colors = [255, 150, 100, 200]
651+
colored_meshes.append(colored_mesh)
652+
if colored_meshes:
653+
mesh = trimesh.util.concatenate(colored_meshes)
654+
else:
655+
mesh = None
656+
else:
657+
mesh = mesh.copy()
658+
mesh.visual.face_colors = [255, 150, 100, 200]
659+
else:
660+
mesh = link.concatenated_visual_mesh
661+
662+
if mesh is not None and len(mesh.vertices) > 0:
663+
# Create pyrender mesh
664+
pyrender_mesh = pyrender.Mesh.from_trimesh(mesh, smooth=False)
665+
666+
# Create node with transformation matrix
667+
node = pyrender.Node(
668+
name=f"{'collision' if self.show_collision else 'visual'}_{link.name}_{link_id}",
669+
mesh=pyrender_mesh,
670+
matrix=transform
671+
)
672+
673+
# Add to scene
674+
self.scene.add_node(node)
675+
676+
# Update visual mesh map for compatibility (only for visual meshes)
677+
if not self.show_collision:
678+
self._visual_mesh_map[link_id] = (node, link)
679+
680+
# Process child links
681+
for child_link in link.child_links:
682+
self._add_single_link_mesh_for_toggle(child_link)

0 commit comments

Comments
 (0)