OpenMotionLab/MotionGPT
118
1"""A pyglet-based interactive 3D scene viewer.2"""3import copy4import os5import sys6from threading import Thread, RLock7import time8 9import imageio10import numpy as np11import OpenGL12import trimesh13 14try:15 from Tkinter import Tk, tkFileDialog as filedialog16except Exception:17 try:18 from tkinter import Tk, filedialog as filedialog19 except Exception:20 pass21 22from .constants import (TARGET_OPEN_GL_MAJOR, TARGET_OPEN_GL_MINOR,23 MIN_OPEN_GL_MAJOR, MIN_OPEN_GL_MINOR,24 TEXT_PADDING, DEFAULT_SCENE_SCALE,25 DEFAULT_Z_FAR, DEFAULT_Z_NEAR, RenderFlags, TextAlign)26from .light import DirectionalLight27from .node import Node28from .camera import PerspectiveCamera, OrthographicCamera, IntrinsicsCamera29from .trackball import Trackball30from .renderer import Renderer31from .mesh import Mesh32 33import pyglet34from pyglet import clock35pyglet.options['shadow_window'] = False36 37 38class Viewer(pyglet.window.Window):39 """An interactive viewer for 3D scenes.40 41 The viewer's camera is separate from the scene's, but will take on42 the parameters of the scene's main view camera and start in the same pose.43 If the scene does not have a camera, a suitable default will be provided.44 45 Parameters46 ----------47 scene : :class:`Scene`48 The scene to visualize.49 viewport_size : (2,) int50 The width and height of the initial viewing window.51 render_flags : dict52 A set of flags for rendering the scene. Described in the note below.53 viewer_flags : dict54 A set of flags for controlling the viewer's behavior.55 Described in the note below.56 registered_keys : dict57 A map from ASCII key characters to tuples containing:58 59 - A function to be called whenever the key is pressed,60 whose first argument will be the viewer itself.61 - (Optionally) A list of additional positional arguments62 to be passed to the function.63 - (Optionally) A dict of keyword arguments to be passed64 to the function.65 66 kwargs : dict67 Any keyword arguments left over will be interpreted as belonging to68 either the :attr:`.Viewer.render_flags` or :attr:`.Viewer.viewer_flags`69 dictionaries. Those flag sets will be updated appropriately.70 71 Note72 ----73 The basic commands for moving about the scene are given as follows:74 75 - **Rotating about the scene**: Hold the left mouse button and76 drag the cursor.77 - **Rotating about the view axis**: Hold ``CTRL`` and the left mouse78 button and drag the cursor.79 - **Panning**:80 81 - Hold SHIFT, then hold the left mouse button and drag the cursor, or82 - Hold the middle mouse button and drag the cursor.83 84 - **Zooming**:85 86 - Scroll the mouse wheel, or87 - Hold the right mouse button and drag the cursor.88 89 Other keyboard commands are as follows:90 91 - ``a``: Toggles rotational animation mode.92 - ``c``: Toggles backface culling.93 - ``f``: Toggles fullscreen mode.94 - ``h``: Toggles shadow rendering.95 - ``i``: Toggles axis display mode96 (no axes, world axis, mesh axes, all axes).97 - ``l``: Toggles lighting mode98 (scene lighting, Raymond lighting, or direct lighting).99 - ``m``: Toggles face normal visualization.100 - ``n``: Toggles vertex normal visualization.101 - ``o``: Toggles orthographic mode.102 - ``q``: Quits the viewer.103 - ``r``: Starts recording a GIF, and pressing again stops recording104 and opens a file dialog.105 - ``s``: Opens a file dialog to save the current view as an image.106 - ``w``: Toggles wireframe mode107 (scene default, flip wireframes, all wireframe, or all solid).108 - ``z``: Resets the camera to the initial view.109 110 Note111 ----112 The valid keys for ``render_flags`` are as follows:113 114 - ``flip_wireframe``: `bool`, If `True`, all objects will have their115 wireframe modes flipped from what their material indicates.116 Defaults to `False`.117 - ``all_wireframe``: `bool`, If `True`, all objects will be rendered118 in wireframe mode. Defaults to `False`.119 - ``all_solid``: `bool`, If `True`, all objects will be rendered in120 solid mode. Defaults to `False`.121 - ``shadows``: `bool`, If `True`, shadows will be rendered.122 Defaults to `False`.123 - ``vertex_normals``: `bool`, If `True`, vertex normals will be124 rendered as blue lines. Defaults to `False`.125 - ``face_normals``: `bool`, If `True`, face normals will be rendered as126 blue lines. Defaults to `False`.127 - ``cull_faces``: `bool`, If `True`, backfaces will be culled.128 Defaults to `True`.129 - ``point_size`` : float, The point size in pixels. Defaults to 1px.130 131 Note132 ----133 The valid keys for ``viewer_flags`` are as follows:134 135 - ``rotate``: `bool`, If `True`, the scene's camera will rotate136 about an axis. Defaults to `False`.137 - ``rotate_rate``: `float`, The rate of rotation in radians per second.138 Defaults to `PI / 3.0`.139 - ``rotate_axis``: `(3,) float`, The axis in world coordinates to rotate140 about. Defaults to ``[0,0,1]``.141 - ``view_center``: `(3,) float`, The position to rotate the scene about.142 Defaults to the scene's centroid.143 - ``use_raymond_lighting``: `bool`, If `True`, an additional set of three144 directional lights that move with the camera will be added to the scene.145 Defaults to `False`.146 - ``use_direct_lighting``: `bool`, If `True`, an additional directional147 light that moves with the camera and points out of it will be added to148 the scene. Defaults to `False`.149 - ``lighting_intensity``: `float`, The overall intensity of the150 viewer's additional lights (when they're in use). Defaults to 3.0.151 - ``use_perspective_cam``: `bool`, If `True`, a perspective camera will152 be used. Otherwise, an orthographic camera is used. Defaults to `True`.153 - ``save_directory``: `str`, A directory to open the file dialogs in.154 Defaults to `None`.155 - ``window_title``: `str`, A title for the viewer's application window.156 Defaults to `"Scene Viewer"`.157 - ``refresh_rate``: `float`, A refresh rate for rendering, in Hertz.158 Defaults to `30.0`.159 - ``fullscreen``: `bool`, Whether to make viewer fullscreen.160 Defaults to `False`.161 - ``show_world_axis``: `bool`, Whether to show the world axis.162 Defaults to `False`.163 - ``show_mesh_axes``: `bool`, Whether to show the individual mesh axes.164 Defaults to `False`.165 - ``caption``: `list of dict`, Text caption(s) to display on the viewer.166 Defaults to `None`.167 168 Note169 ----170 Animation can be accomplished by running the viewer with ``run_in_thread``171 enabled. Then, just run a loop in your main thread, updating the scene as172 needed. Before updating the scene, be sure to acquire the173 :attr:`.Viewer.render_lock`, and release it when your update is done.174 """175 176 def __init__(self, scene, viewport_size=None,177 render_flags=None, viewer_flags=None,178 registered_keys=None, run_in_thread=False,179 auto_start=True,180 **kwargs):181 182 #######################################################################183 # Save attributes and flags184 #######################################################################185 if viewport_size is None:186 viewport_size = (640, 480)187 self._scene = scene188 self._viewport_size = viewport_size189 self._render_lock = RLock()190 self._is_active = False191 self._should_close = False192 self._run_in_thread = run_in_thread193 self._auto_start = auto_start194 195 self._default_render_flags = {196 'flip_wireframe': False,197 'all_wireframe': False,198 'all_solid': False,199 'shadows': False,200 'vertex_normals': False,201 'face_normals': False,202 'cull_faces': True,203 'point_size': 1.0,204 }205 self._default_viewer_flags = {206 'mouse_pressed': False,207 'rotate': False,208 'rotate_rate': np.pi / 3.0,209 'rotate_axis': np.array([0.0, 0.0, 1.0]),210 'view_center': None,211 'record': False,212 'use_raymond_lighting': False,213 'use_direct_lighting': False,214 'lighting_intensity': 3.0,215 'use_perspective_cam': True,216 'save_directory': None,217 'window_title': 'Scene Viewer',218 'refresh_rate': 30.0,219 'fullscreen': False,220 'show_world_axis': False,221 'show_mesh_axes': False,222 'caption': None223 }224 self._render_flags = self._default_render_flags.copy()225 self._viewer_flags = self._default_viewer_flags.copy()226 self._viewer_flags['rotate_axis'] = (227 self._default_viewer_flags['rotate_axis'].copy()228 )229 230 if render_flags is not None:231 self._render_flags.update(render_flags)232 if viewer_flags is not None:233 self._viewer_flags.update(viewer_flags)234 235 for key in kwargs:236 if key in self.render_flags:237 self._render_flags[key] = kwargs[key]238 elif key in self.viewer_flags:239 self._viewer_flags[key] = kwargs[key]240 241 # TODO MAC OS BUG FOR SHADOWS242 if sys.platform == 'darwin':243 self._render_flags['shadows'] = False244 245 self._registered_keys = {}246 if registered_keys is not None:247 self._registered_keys = {248 ord(k.lower()): registered_keys[k] for k in registered_keys249 }250 251 #######################################################################252 # Save internal settings253 #######################################################################254 255 # Set up caption stuff256 self._message_text = None257 self._ticks_till_fade = 2.0 / 3.0 * self.viewer_flags['refresh_rate']258 self._message_opac = 1.0 + self._ticks_till_fade259 260 # Set up raymond lights and direct lights261 self._raymond_lights = self._create_raymond_lights()262 self._direct_light = self._create_direct_light()263 264 # Set up axes265 self._axes = {}266 self._axis_mesh = Mesh.from_trimesh(267 trimesh.creation.axis(origin_size=0.1, axis_radius=0.05,268 axis_length=1.0), smooth=False)269 if self.viewer_flags['show_world_axis']:270 self._set_axes(world=self.viewer_flags['show_world_axis'],271 mesh=self.viewer_flags['show_mesh_axes'])272 273 #######################################################################274 # Set up camera node275 #######################################################################276 self._camera_node = None277 self._prior_main_camera_node = None278 self._default_camera_pose = None279 self._default_persp_cam = None280 self._default_orth_cam = None281 self._trackball = None282 self._saved_frames = []283 284 # Extract main camera from scene and set up our mirrored copy285 znear = None286 zfar = None287 if scene.main_camera_node is not None:288 n = scene.main_camera_node289 camera = copy.copy(n.camera)290 if isinstance(camera, (PerspectiveCamera, IntrinsicsCamera)):291 self._default_persp_cam = camera292 znear = camera.znear293 zfar = camera.zfar294 elif isinstance(camera, OrthographicCamera):295 self._default_orth_cam = camera296 znear = camera.znear297 zfar = camera.zfar298 self._default_camera_pose = scene.get_pose(scene.main_camera_node)299 self._prior_main_camera_node = n300 301 # Set defaults as needed302 if zfar is None:303 zfar = max(scene.scale * 10.0, DEFAULT_Z_FAR)304 if znear is None or znear == 0:305 if scene.scale == 0:306 znear = DEFAULT_Z_NEAR307 else:308 znear = min(scene.scale / 10.0, DEFAULT_Z_NEAR)309 310 if self._default_persp_cam is None:311 self._default_persp_cam = PerspectiveCamera(312 yfov=np.pi / 3.0, znear=znear, zfar=zfar313 )314 if self._default_orth_cam is None:315 xmag = ymag = scene.scale316 if scene.scale == 0:317 xmag = ymag = 1.0318 self._default_orth_cam = OrthographicCamera(319 xmag=xmag, ymag=ymag,320 znear=znear,321 zfar=zfar322 )323 if self._default_camera_pose is None:324 self._default_camera_pose = self._compute_initial_camera_pose()325 326 # Pick camera327 if self.viewer_flags['use_perspective_cam']:328 camera = self._default_persp_cam329 else:330 camera = self._default_orth_cam331 332 self._camera_node = Node(333 matrix=self._default_camera_pose, camera=camera334 )335 scene.add_node(self._camera_node)336 scene.main_camera_node = self._camera_node337 self._reset_view()338 339 #######################################################################340 # Initialize OpenGL context and renderer341 #######################################################################342 self._renderer = Renderer(343 self._viewport_size[0], self._viewport_size[1],344 self.render_flags['point_size']345 )346 self._is_active = True347 348 if self.run_in_thread:349 self._thread = Thread(target=self._init_and_start_app)350 self._thread.start()351 else:352 if auto_start:353 self._init_and_start_app()354 355 def start(self):356 self._init_and_start_app()357 358 @property359 def scene(self):360 """:class:`.Scene` : The scene being visualized.361 """362 return self._scene363 364 @property365 def viewport_size(self):366 """(2,) int : The width and height of the viewing window.367 """368 return self._viewport_size369 370 @property371 def render_lock(self):372 """:class:`threading.RLock` : If acquired, prevents the viewer from373 rendering until released.374 375 Run :meth:`.Viewer.render_lock.acquire` before making updates to376 the scene in a different thread, and run377 :meth:`.Viewer.render_lock.release` once you're done to let the viewer378 continue.379 """380 return self._render_lock381 382 @property383 def is_active(self):384 """bool : `True` if the viewer is active, or `False` if it has385 been closed.386 """387 return self._is_active388 389 @property390 def run_in_thread(self):391 """bool : Whether the viewer was run in a separate thread.392 """393 return self._run_in_thread394 395 @property396 def render_flags(self):397 """dict : Flags for controlling the renderer's behavior.398 399 - ``flip_wireframe``: `bool`, If `True`, all objects will have their400 wireframe modes flipped from what their material indicates.401 Defaults to `False`.402 - ``all_wireframe``: `bool`, If `True`, all objects will be rendered403 in wireframe mode. Defaults to `False`.404 - ``all_solid``: `bool`, If `True`, all objects will be rendered in405 solid mode. Defaults to `False`.406 - ``shadows``: `bool`, If `True`, shadows will be rendered.407 Defaults to `False`.408 - ``vertex_normals``: `bool`, If `True`, vertex normals will be409 rendered as blue lines. Defaults to `False`.410 - ``face_normals``: `bool`, If `True`, face normals will be rendered as411 blue lines. Defaults to `False`.412 - ``cull_faces``: `bool`, If `True`, backfaces will be culled.413 Defaults to `True`.414 - ``point_size`` : float, The point size in pixels. Defaults to 1px.415 416 """417 return self._render_flags418 419 @render_flags.setter420 def render_flags(self, value):421 self._render_flags = value422 423 @property424 def viewer_flags(self):425 """dict : Flags for controlling the viewer's behavior.426 427 The valid keys for ``viewer_flags`` are as follows:428 429 - ``rotate``: `bool`, If `True`, the scene's camera will rotate430 about an axis. Defaults to `False`.431 - ``rotate_rate``: `float`, The rate of rotation in radians per second.432 Defaults to `PI / 3.0`.433 - ``rotate_axis``: `(3,) float`, The axis in world coordinates to434 rotate about. Defaults to ``[0,0,1]``.435 - ``view_center``: `(3,) float`, The position to rotate the scene436 about. Defaults to the scene's centroid.437 - ``use_raymond_lighting``: `bool`, If `True`, an additional set of438 three directional lights that move with the camera will be added to439 the scene. Defaults to `False`.440 - ``use_direct_lighting``: `bool`, If `True`, an additional directional441 light that moves with the camera and points out of it will be442 added to the scene. Defaults to `False`.443 - ``lighting_intensity``: `float`, The overall intensity of the444 viewer's additional lights (when they're in use). Defaults to 3.0.445 - ``use_perspective_cam``: `bool`, If `True`, a perspective camera will446 be used. Otherwise, an orthographic camera is used. Defaults to447 `True`.448 - ``save_directory``: `str`, A directory to open the file dialogs in.449 Defaults to `None`.450 - ``window_title``: `str`, A title for the viewer's application window.451 Defaults to `"Scene Viewer"`.452 - ``refresh_rate``: `float`, A refresh rate for rendering, in Hertz.453 Defaults to `30.0`.454 - ``fullscreen``: `bool`, Whether to make viewer fullscreen.455 Defaults to `False`.456 - ``show_world_axis``: `bool`, Whether to show the world axis.457 Defaults to `False`.458 - ``show_mesh_axes``: `bool`, Whether to show the individual mesh axes.459 Defaults to `False`.460 - ``caption``: `list of dict`, Text caption(s) to display on461 the viewer. Defaults to `None`.462 463 """464 return self._viewer_flags465 466 @viewer_flags.setter467 def viewer_flags(self, value):468 self._viewer_flags = value469 470 @property471 def registered_keys(self):472 """dict : Map from ASCII key character to a handler function.473 474 This is a map from ASCII key characters to tuples containing:475 476 - A function to be called whenever the key is pressed,477 whose first argument will be the viewer itself.478 - (Optionally) A list of additional positional arguments479 to be passed to the function.480 - (Optionally) A dict of keyword arguments to be passed481 to the function.482 483 """484 return self._registered_keys485 486 @registered_keys.setter487 def registered_keys(self, value):488 self._registered_keys = value489 490 def close_external(self):491 """Close the viewer from another thread.492 493 This function will wait for the actual close, so you immediately494 manipulate the scene afterwards.495 """496 self._should_close = True497 while self.is_active:498 time.sleep(1.0 / self.viewer_flags['refresh_rate'])499 500 def save_gif(self, filename=None):501 """Save the stored GIF frames to a file.502 503 To use this asynchronously, run the viewer with the ``record``504 flag and the ``run_in_thread`` flags set.505 Kill the viewer after your desired time with506 :meth:`.Viewer.close_external`, and then call :meth:`.Viewer.save_gif`.507 508 Parameters509 ----------510 filename : str511 The file to save the GIF to. If not specified,512 a file dialog will be opened to ask the user where513 to save the GIF file.514 """515 if filename is None:516 filename = self._get_save_filename(['gif', 'all'])517 if filename is not None:518 self.viewer_flags['save_directory'] = os.path.dirname(filename)519 imageio.mimwrite(filename, self._saved_frames,520 fps=self.viewer_flags['refresh_rate'],521 palettesize=128, subrectangles=True)522 self._saved_frames = []523 524 def on_close(self):525 """Exit the event loop when the window is closed.526 """527 # Remove our camera and restore the prior one528 if self._camera_node is not None:529 self.scene.remove_node(self._camera_node)530 if self._prior_main_camera_node is not None:531 self.scene.main_camera_node = self._prior_main_camera_node532 533 # Delete any lighting nodes that we've attached534 if self.viewer_flags['use_raymond_lighting']:535 for n in self._raymond_lights:536 if self.scene.has_node(n):537 self.scene.remove_node(n)538 if self.viewer_flags['use_direct_lighting']:539 if self.scene.has_node(self._direct_light):540 self.scene.remove_node(self._direct_light)541 542 # Delete any axis nodes that we've attached543 self._remove_axes()544 545 # Delete renderer546 if self._renderer is not None:547 self._renderer.delete()548 self._renderer = None549 550 # Force clean-up of OpenGL context data551 try:552 OpenGL.contextdata.cleanupContext()553 self.close()554 except Exception:555 pass556 finally:557 self._is_active = False558 super(Viewer, self).on_close()559 pyglet.app.exit()560 561 def on_draw(self):562 """Redraw the scene into the viewing window.563 """564 if self._renderer is None:565 return566 567 if self.run_in_thread or not self._auto_start:568 self.render_lock.acquire()569 570 # Make OpenGL context current571 self.switch_to()572 573 # Render the scene574 self.clear()575 self._render()576 577 if self._message_text is not None:578 self._renderer.render_text(579 self._message_text,580 self.viewport_size[0] - TEXT_PADDING,581 TEXT_PADDING,582 font_pt=20,583 color=np.array([0.1, 0.7, 0.2,584 np.clip(self._message_opac, 0.0, 1.0)]),585 align=TextAlign.BOTTOM_RIGHT586 )587 588 if self.viewer_flags['caption'] is not None:589 for caption in self.viewer_flags['caption']:590 xpos, ypos = self._location_to_x_y(caption['location'])591 self._renderer.render_text(592 caption['text'],593 xpos,594 ypos,595 font_name=caption['font_name'],596 font_pt=caption['font_pt'],597 color=caption['color'],598 scale=caption['scale'],599 align=caption['location']600 )601 602 if self.run_in_thread or not self._auto_start:603 self.render_lock.release()604 605 def on_resize(self, width, height):606 """Resize the camera and trackball when the window is resized.607 """608 if self._renderer is None:609 return610 611 self._viewport_size = (width, height)612 self._trackball.resize(self._viewport_size)613 self._renderer.viewport_width = self._viewport_size[0]614 self._renderer.viewport_height = self._viewport_size[1]615 self.on_draw()616 617 def on_mouse_press(self, x, y, buttons, modifiers):618 """Record an initial mouse press.619 """620 self._trackball.set_state(Trackball.STATE_ROTATE)621 if (buttons == pyglet.window.mouse.LEFT):622 ctrl = (modifiers & pyglet.window.key.MOD_CTRL)623 shift = (modifiers & pyglet.window.key.MOD_SHIFT)624 if (ctrl and shift):625 self._trackball.set_state(Trackball.STATE_ZOOM)626 elif ctrl:627 self._trackball.set_state(Trackball.STATE_ROLL)628 elif shift:629 self._trackball.set_state(Trackball.STATE_PAN)630 elif (buttons == pyglet.window.mouse.MIDDLE):631 self._trackball.set_state(Trackball.STATE_PAN)632 elif (buttons == pyglet.window.mouse.RIGHT):633 self._trackball.set_state(Trackball.STATE_ZOOM)634 635 self._trackball.down(np.array([x, y]))636 637 # Stop animating while using the mouse638 self.viewer_flags['mouse_pressed'] = True639 640 def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):641 """Record a mouse drag.642 """643 self._trackball.drag(np.array([x, y]))644 645 def on_mouse_release(self, x, y, button, modifiers):646 """Record a mouse release.647 """648 self.viewer_flags['mouse_pressed'] = False649 650 def on_mouse_scroll(self, x, y, dx, dy):651 """Record a mouse scroll.652 """653 if self.viewer_flags['use_perspective_cam']:654 self._trackball.scroll(dy)655 else:656 spfc = 0.95657 spbc = 1.0 / 0.95658 sf = 1.0659 if dy > 0:660 sf = spfc * dy661 elif dy < 0:662 sf = - spbc * dy663 664 c = self._camera_node.camera665 xmag = max(c.xmag * sf, 1e-8)666 ymag = max(c.ymag * sf, 1e-8 * c.ymag / c.xmag)667 c.xmag = xmag668 c.ymag = ymag669 670 def on_key_press(self, symbol, modifiers):671 """Record a key press.672 """673 # First, check for registered key callbacks674 if symbol in self.registered_keys:675 tup = self.registered_keys[symbol]676 callback = None677 args = []678 kwargs = {}679 if not isinstance(tup, (list, tuple, np.ndarray)):680 callback = tup681 else:682 callback = tup[0]683 if len(tup) == 2:684 args = tup[1]685 if len(tup) == 3:686 kwargs = tup[2]687 callback(self, *args, **kwargs)688 return689 690 # Otherwise, use default key functions691 692 # A causes the frame to rotate693 self._message_text = None694 if symbol == pyglet.window.key.A:695 self.viewer_flags['rotate'] = not self.viewer_flags['rotate']696 if self.viewer_flags['rotate']:697 self._message_text = 'Rotation On'698 else:699 self._message_text = 'Rotation Off'700 701 # C toggles backface culling702 elif symbol == pyglet.window.key.C:703 self.render_flags['cull_faces'] = (704 not self.render_flags['cull_faces']705 )706 if self.render_flags['cull_faces']:707 self._message_text = 'Cull Faces On'708 else:709 self._message_text = 'Cull Faces Off'710 711 # F toggles face normals712 elif symbol == pyglet.window.key.F:713 self.viewer_flags['fullscreen'] = (714 not self.viewer_flags['fullscreen']715 )716 self.set_fullscreen(self.viewer_flags['fullscreen'])717 self.activate()718 if self.viewer_flags['fullscreen']:719 self._message_text = 'Fullscreen On'720 else:721 self._message_text = 'Fullscreen Off'722 723 # S toggles shadows724 elif symbol == pyglet.window.key.H and sys.platform != 'darwin':725 self.render_flags['shadows'] = not self.render_flags['shadows']726 if self.render_flags['shadows']:727 self._message_text = 'Shadows On'728 else:729 self._message_text = 'Shadows Off'730 731 elif symbol == pyglet.window.key.I:732 if (self.viewer_flags['show_world_axis'] and not733 self.viewer_flags['show_mesh_axes']):734 self.viewer_flags['show_world_axis'] = False735 self.viewer_flags['show_mesh_axes'] = True736 self._set_axes(False, True)737 self._message_text = 'Mesh Axes On'738 elif (not self.viewer_flags['show_world_axis'] and739 self.viewer_flags['show_mesh_axes']):740 self.viewer_flags['show_world_axis'] = True741 self.viewer_flags['show_mesh_axes'] = True742 self._set_axes(True, True)743 self._message_text = 'All Axes On'744 elif (self.viewer_flags['show_world_axis'] and745 self.viewer_flags['show_mesh_axes']):746 self.viewer_flags['show_world_axis'] = False747 self.viewer_flags['show_mesh_axes'] = False748 self._set_axes(False, False)749 self._message_text = 'All Axes Off'750 else:751 self.viewer_flags['show_world_axis'] = True752 self.viewer_flags['show_mesh_axes'] = False753 self._set_axes(True, False)754 self._message_text = 'World Axis On'755 756 # L toggles the lighting mode757 elif symbol == pyglet.window.key.L:758 if self.viewer_flags['use_raymond_lighting']:759 self.viewer_flags['use_raymond_lighting'] = False760 self.viewer_flags['use_direct_lighting'] = True761 self._message_text = 'Direct Lighting'762 elif self.viewer_flags['use_direct_lighting']:763 self.viewer_flags['use_raymond_lighting'] = False764 self.viewer_flags['use_direct_lighting'] = False765 self._message_text = 'Default Lighting'766 else:767 self.viewer_flags['use_raymond_lighting'] = True768 self.viewer_flags['use_direct_lighting'] = False769 self._message_text = 'Raymond Lighting'770 771 # M toggles face normals772 elif symbol == pyglet.window.key.M:773 self.render_flags['face_normals'] = (774 not self.render_flags['face_normals']775 )776 if self.render_flags['face_normals']:777 self._message_text = 'Face Normals On'778 else:779 self._message_text = 'Face Normals Off'780 781 # N toggles vertex normals782 elif symbol == pyglet.window.key.N:783 self.render_flags['vertex_normals'] = (784 not self.render_flags['vertex_normals']785 )786 if self.render_flags['vertex_normals']:787 self._message_text = 'Vert Normals On'788 else:789 self._message_text = 'Vert Normals Off'790 791 # O toggles orthographic camera mode792 elif symbol == pyglet.window.key.O:793 self.viewer_flags['use_perspective_cam'] = (794 not self.viewer_flags['use_perspective_cam']795 )796 if self.viewer_flags['use_perspective_cam']:797 camera = self._default_persp_cam798 self._message_text = 'Perspective View'799 else:800 camera = self._default_orth_cam801 self._message_text = 'Orthographic View'802 803 cam_pose = self._camera_node.matrix.copy()804 cam_node = Node(matrix=cam_pose, camera=camera)805 self.scene.remove_node(self._camera_node)806 self.scene.add_node(cam_node)807 self.scene.main_camera_node = cam_node808 self._camera_node = cam_node809 810 # Q quits the viewer811 elif symbol == pyglet.window.key.Q:812 self.on_close()813 814 # R starts recording frames815 elif symbol == pyglet.window.key.R:816 if self.viewer_flags['record']:817 self.save_gif()818 self.set_caption(self.viewer_flags['window_title'])819 else:820 self.set_caption(821 '{} (RECORDING)'.format(self.viewer_flags['window_title'])822 )823 self.viewer_flags['record'] = not self.viewer_flags['record']824 825 # S saves the current frame as an image826 elif symbol == pyglet.window.key.S:827 self._save_image()828 829 # W toggles through wireframe modes830 elif symbol == pyglet.window.key.W:831 if self.render_flags['flip_wireframe']:832 self.render_flags['flip_wireframe'] = False833 self.render_flags['all_wireframe'] = True834 self.render_flags['all_solid'] = False835 self._message_text = 'All Wireframe'836 elif self.render_flags['all_wireframe']:837 self.render_flags['flip_wireframe'] = False838 self.render_flags['all_wireframe'] = False839 self.render_flags['all_solid'] = True840 self._message_text = 'All Solid'841 elif self.render_flags['all_solid']:842 self.render_flags['flip_wireframe'] = False843 self.render_flags['all_wireframe'] = False844 self.render_flags['all_solid'] = False845 self._message_text = 'Default Wireframe'846 else:847 self.render_flags['flip_wireframe'] = True848 self.render_flags['all_wireframe'] = False849 self.render_flags['all_solid'] = False850 self._message_text = 'Flip Wireframe'851 852 # Z resets the camera viewpoint853 elif symbol == pyglet.window.key.Z:854 self._reset_view()855 856 if self._message_text is not None:857 self._message_opac = 1.0 + self._ticks_till_fade858 859 @staticmethod860 def _time_event(dt, self):861 """The timer callback.862 """863 # Don't run old dead events after we've already closed864 if not self._is_active:865 return866 867 if self.viewer_flags['record']:868 self._record()869 if (self.viewer_flags['rotate'] and not870 self.viewer_flags['mouse_pressed']):871 self._rotate()872 873 # Manage message opacity874 if self._message_text is not None:875 if self._message_opac > 1.0:876 self._message_opac -= 1.0877 else:878 self._message_opac *= 0.90879 if self._message_opac < 0.05:880 self._message_opac = 1.0 + self._ticks_till_fade881 self._message_text = None882 883 if self._should_close:884 self.on_close()885 else:886 self.on_draw()887 888 def _reset_view(self):889 """Reset the view to a good initial state.890 891 The view is initially along the positive x-axis at a892 sufficient distance from the scene.893 """894 scale = self.scene.scale895 if scale == 0.0:896 scale = DEFAULT_SCENE_SCALE897 centroid = self.scene.centroid898 899 if self.viewer_flags['view_center'] is not None:900 centroid = self.viewer_flags['view_center']901 902 self._camera_node.matrix = self._default_camera_pose903 self._trackball = Trackball(904 self._default_camera_pose, self.viewport_size, scale, centroid905 )906 907 def _get_save_filename(self, file_exts):908 file_types = {909 'png': ('png files', '*.png'),910 'jpg': ('jpeg files', '*.jpg'),911 'gif': ('gif files', '*.gif'),912 'all': ('all files', '*'),913 }914 filetypes = [file_types[x] for x in file_exts]915 try:916 root = Tk()917 save_dir = self.viewer_flags['save_directory']918 if save_dir is None:919 save_dir = os.getcwd()920 filename = filedialog.asksaveasfilename(921 initialdir=save_dir, title='Select file save location',922 filetypes=filetypes923 )924 except Exception:925 return None926 927 root.destroy()928 if filename == ():929 return None930 return filename931 932 def _save_image(self):933 filename = self._get_save_filename(['png', 'jpg', 'gif', 'all'])934 if filename is not None:935 self.viewer_flags['save_directory'] = os.path.dirname(filename)936 imageio.imwrite(filename, self._renderer.read_color_buf())937 938 def _record(self):939 """Save another frame for the GIF.940 """941 data = self._renderer.read_color_buf()942 if not np.all(data == 0.0):943 self._saved_frames.append(data)944 945 def _rotate(self):946 """Animate the scene by rotating the camera.947 """948 az = (self.viewer_flags['rotate_rate'] /949 self.viewer_flags['refresh_rate'])950 self._trackball.rotate(az, self.viewer_flags['rotate_axis'])951 952 def _render(self):953 """Render the scene into the framebuffer and flip.954 """955 scene = self.scene956 self._camera_node.matrix = self._trackball.pose.copy()957 958 # Set lighting959 vli = self.viewer_flags['lighting_intensity']960 if self.viewer_flags['use_raymond_lighting']:961 for n in self._raymond_lights:962 n.light.intensity = vli / 3.0963 if not self.scene.has_node(n):964 scene.add_node(n, parent_node=self._camera_node)965 else:966 self._direct_light.light.intensity = vli967 for n in self._raymond_lights:968 if self.scene.has_node(n):969 self.scene.remove_node(n)970 971 if self.viewer_flags['use_direct_lighting']:972 if not self.scene.has_node(self._direct_light):973 scene.add_node(974 self._direct_light, parent_node=self._camera_node975 )976 elif self.scene.has_node(self._direct_light):977 self.scene.remove_node(self._direct_light)978 979 flags = RenderFlags.NONE980 if self.render_flags['flip_wireframe']:981 flags |= RenderFlags.FLIP_WIREFRAME982 elif self.render_flags['all_wireframe']:983 flags |= RenderFlags.ALL_WIREFRAME984 elif self.render_flags['all_solid']:985 flags |= RenderFlags.ALL_SOLID986 987 if self.render_flags['shadows']:988 flags |= RenderFlags.SHADOWS_DIRECTIONAL | RenderFlags.SHADOWS_SPOT989 if self.render_flags['vertex_normals']:990 flags |= RenderFlags.VERTEX_NORMALS991 if self.render_flags['face_normals']:992 flags |= RenderFlags.FACE_NORMALS993 if not self.render_flags['cull_faces']:994 flags |= RenderFlags.SKIP_CULL_FACES995 996 self._renderer.render(self.scene, flags)997 998 def _init_and_start_app(self):999 # Try multiple configs starting with target OpenGL version1000 # and multisampling and removing these options if exception1001 # Note: multisampling not available on all hardware1002 from pyglet.gl import Config1003 confs = [Config(sample_buffers=1, samples=4,1004 depth_size=24,1005 double_buffer=True,1006 major_version=TARGET_OPEN_GL_MAJOR,1007 minor_version=TARGET_OPEN_GL_MINOR),1008 Config(depth_size=24,1009 double_buffer=True,1010 major_version=TARGET_OPEN_GL_MAJOR,1011 minor_version=TARGET_OPEN_GL_MINOR),1012 Config(sample_buffers=1, samples=4,1013 depth_size=24,1014 double_buffer=True,1015 major_version=MIN_OPEN_GL_MAJOR,1016 minor_version=MIN_OPEN_GL_MINOR),1017 Config(depth_size=24,1018 double_buffer=True,1019 major_version=MIN_OPEN_GL_MAJOR,1020 minor_version=MIN_OPEN_GL_MINOR)]1021 for conf in confs:1022 try:1023 super(Viewer, self).__init__(config=conf, resizable=True,1024 width=self._viewport_size[0],1025 height=self._viewport_size[1])1026 break1027 except pyglet.window.NoSuchConfigException:1028 pass1029 1030 if not self.context:1031 raise ValueError('Unable to initialize an OpenGL 3+ context')1032 clock.schedule_interval(1033 Viewer._time_event, 1.0 / self.viewer_flags['refresh_rate'], self1034 )1035 self.switch_to()1036 self.set_caption(self.viewer_flags['window_title'])1037 pyglet.app.run()1038 1039 def _compute_initial_camera_pose(self):1040 centroid = self.scene.centroid1041 if self.viewer_flags['view_center'] is not None:1042 centroid = self.viewer_flags['view_center']1043 scale = self.scene.scale1044 if scale == 0.0:1045 scale = DEFAULT_SCENE_SCALE1046 1047 s2 = 1.0 / np.sqrt(2.0)1048 cp = np.eye(4)1049 cp[:3,:3] = np.array([1050 [0.0, -s2, s2],1051 [1.0, 0.0, 0.0],1052 [0.0, s2, s2]1053 ])1054 hfov = np.pi / 6.01055 dist = scale / (2.0 * np.tan(hfov))1056 cp[:3,3] = dist * np.array([1.0, 0.0, 1.0]) + centroid1057 1058 return cp1059 1060 def _create_raymond_lights(self):1061 thetas = np.pi * np.array([1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0])1062 phis = np.pi * np.array([0.0, 2.0 / 3.0, 4.0 / 3.0])1063 1064 nodes = []1065 1066 for phi, theta in zip(phis, thetas):1067 xp = np.sin(theta) * np.cos(phi)1068 yp = np.sin(theta) * np.sin(phi)1069 zp = np.cos(theta)1070 1071 z = np.array([xp, yp, zp])1072 z = z / np.linalg.norm(z)1073 x = np.array([-z[1], z[0], 0.0])1074 if np.linalg.norm(x) == 0:1075 x = np.array([1.0, 0.0, 0.0])1076 x = x / np.linalg.norm(x)1077 y = np.cross(z, x)1078 1079 matrix = np.eye(4)1080 matrix[:3,:3] = np.c_[x,y,z]1081 nodes.append(Node(1082 light=DirectionalLight(color=np.ones(3), intensity=1.0),1083 matrix=matrix1084 ))1085 1086 return nodes1087 1088 def _create_direct_light(self):1089 light = DirectionalLight(color=np.ones(3), intensity=1.0)1090 n = Node(light=light, matrix=np.eye(4))1091 return n1092 1093 def _set_axes(self, world, mesh):1094 scale = self.scene.scale1095 if world:1096 if 'scene' not in self._axes:1097 n = Node(mesh=self._axis_mesh, scale=np.ones(3) * scale * 0.3)1098 self.scene.add_node(n)1099 self._axes['scene'] = n1100 else:1101 if 'scene' in self._axes:1102 self.scene.remove_node(self._axes['scene'])1103 self._axes.pop('scene')1104 1105 if mesh:1106 old_nodes = []1107 existing_axes = set([self._axes[k] for k in self._axes])1108 for node in self.scene.mesh_nodes:1109 if node not in existing_axes:1110 old_nodes.append(node)1111 1112 for node in old_nodes:1113 if node in self._axes:1114 continue1115 n = Node(1116 mesh=self._axis_mesh,1117 scale=np.ones(3) * node.mesh.scale * 0.51118 )1119 self.scene.add_node(n, parent_node=node)1120 self._axes[node] = n1121 else:1122 to_remove = set()1123 for main_node in self._axes:1124 if main_node in self.scene.mesh_nodes:1125 self.scene.remove_node(self._axes[main_node])1126 to_remove.add(main_node)1127 for main_node in to_remove:1128 self._axes.pop(main_node)1129 1130 def _remove_axes(self):1131 for main_node in self._axes:1132 axis_node = self._axes[main_node]1133 self.scene.remove_node(axis_node)1134 self._axes = {}1135 1136 def _location_to_x_y(self, location):1137 if location == TextAlign.CENTER:1138 return (self.viewport_size[0] / 2.0, self.viewport_size[1] / 2.0)1139 elif location == TextAlign.CENTER_LEFT:1140 return (TEXT_PADDING, self.viewport_size[1] / 2.0)1141 elif location == TextAlign.CENTER_RIGHT:1142 return (self.viewport_size[0] - TEXT_PADDING,1143 self.viewport_size[1] / 2.0)1144 elif location == TextAlign.BOTTOM_LEFT:1145 return (TEXT_PADDING, TEXT_PADDING)1146 elif location == TextAlign.BOTTOM_RIGHT:1147 return (self.viewport_size[0] - TEXT_PADDING, TEXT_PADDING)1148 elif location == TextAlign.BOTTOM_CENTER:1149 return (self.viewport_size[0] / 2.0, TEXT_PADDING)1150 elif location == TextAlign.TOP_LEFT:1151 return (TEXT_PADDING, self.viewport_size[1] - TEXT_PADDING)1152 elif location == TextAlign.TOP_RIGHT:1153 return (self.viewport_size[0] - TEXT_PADDING,1154 self.viewport_size[1] - TEXT_PADDING)1155 elif location == TextAlign.TOP_CENTER:1156 return (self.viewport_size[0] / 2.0,1157 self.viewport_size[1] - TEXT_PADDING)1158 1159 1160__all__ = ['Viewer']1161 