Spaces:
Runtime error
Runtime error
| """A pyglet-based interactive 3D scene viewer. | |
| """ | |
| import copy | |
| import os | |
| import sys | |
| from threading import Thread, RLock | |
| import time | |
| import imageio | |
| import numpy as np | |
| import OpenGL | |
| import trimesh | |
| try: | |
| from Tkinter import Tk, tkFileDialog as filedialog | |
| except Exception: | |
| try: | |
| from tkinter import Tk, filedialog as filedialog | |
| except Exception: | |
| pass | |
| from .constants import (TARGET_OPEN_GL_MAJOR, TARGET_OPEN_GL_MINOR, | |
| MIN_OPEN_GL_MAJOR, MIN_OPEN_GL_MINOR, | |
| TEXT_PADDING, DEFAULT_SCENE_SCALE, | |
| DEFAULT_Z_FAR, DEFAULT_Z_NEAR, RenderFlags, TextAlign) | |
| from .light import DirectionalLight | |
| from .node import Node | |
| from .camera import PerspectiveCamera, OrthographicCamera, IntrinsicsCamera | |
| from .trackball import Trackball | |
| from .renderer import Renderer | |
| from .mesh import Mesh | |
| import pyglet | |
| from pyglet import clock | |
| pyglet.options['shadow_window'] = False | |
| class Viewer(pyglet.window.Window): | |
| """An interactive viewer for 3D scenes. | |
| The viewer's camera is separate from the scene's, but will take on | |
| the parameters of the scene's main view camera and start in the same pose. | |
| If the scene does not have a camera, a suitable default will be provided. | |
| Parameters | |
| ---------- | |
| scene : :class:`Scene` | |
| The scene to visualize. | |
| viewport_size : (2,) int | |
| The width and height of the initial viewing window. | |
| render_flags : dict | |
| A set of flags for rendering the scene. Described in the note below. | |
| viewer_flags : dict | |
| A set of flags for controlling the viewer's behavior. | |
| Described in the note below. | |
| registered_keys : dict | |
| A map from ASCII key characters to tuples containing: | |
| - A function to be called whenever the key is pressed, | |
| whose first argument will be the viewer itself. | |
| - (Optionally) A list of additional positional arguments | |
| to be passed to the function. | |
| - (Optionally) A dict of keyword arguments to be passed | |
| to the function. | |
| kwargs : dict | |
| Any keyword arguments left over will be interpreted as belonging to | |
| either the :attr:`.Viewer.render_flags` or :attr:`.Viewer.viewer_flags` | |
| dictionaries. Those flag sets will be updated appropriately. | |
| Note | |
| ---- | |
| The basic commands for moving about the scene are given as follows: | |
| - **Rotating about the scene**: Hold the left mouse button and | |
| drag the cursor. | |
| - **Rotating about the view axis**: Hold ``CTRL`` and the left mouse | |
| button and drag the cursor. | |
| - **Panning**: | |
| - Hold SHIFT, then hold the left mouse button and drag the cursor, or | |
| - Hold the middle mouse button and drag the cursor. | |
| - **Zooming**: | |
| - Scroll the mouse wheel, or | |
| - Hold the right mouse button and drag the cursor. | |
| Other keyboard commands are as follows: | |
| - ``a``: Toggles rotational animation mode. | |
| - ``c``: Toggles backface culling. | |
| - ``f``: Toggles fullscreen mode. | |
| - ``h``: Toggles shadow rendering. | |
| - ``i``: Toggles axis display mode | |
| (no axes, world axis, mesh axes, all axes). | |
| - ``l``: Toggles lighting mode | |
| (scene lighting, Raymond lighting, or direct lighting). | |
| - ``m``: Toggles face normal visualization. | |
| - ``n``: Toggles vertex normal visualization. | |
| - ``o``: Toggles orthographic mode. | |
| - ``q``: Quits the viewer. | |
| - ``r``: Starts recording a GIF, and pressing again stops recording | |
| and opens a file dialog. | |
| - ``s``: Opens a file dialog to save the current view as an image. | |
| - ``w``: Toggles wireframe mode | |
| (scene default, flip wireframes, all wireframe, or all solid). | |
| - ``z``: Resets the camera to the initial view. | |
| Note | |
| ---- | |
| The valid keys for ``render_flags`` are as follows: | |
| - ``flip_wireframe``: `bool`, If `True`, all objects will have their | |
| wireframe modes flipped from what their material indicates. | |
| Defaults to `False`. | |
| - ``all_wireframe``: `bool`, If `True`, all objects will be rendered | |
| in wireframe mode. Defaults to `False`. | |
| - ``all_solid``: `bool`, If `True`, all objects will be rendered in | |
| solid mode. Defaults to `False`. | |
| - ``shadows``: `bool`, If `True`, shadows will be rendered. | |
| Defaults to `False`. | |
| - ``vertex_normals``: `bool`, If `True`, vertex normals will be | |
| rendered as blue lines. Defaults to `False`. | |
| - ``face_normals``: `bool`, If `True`, face normals will be rendered as | |
| blue lines. Defaults to `False`. | |
| - ``cull_faces``: `bool`, If `True`, backfaces will be culled. | |
| Defaults to `True`. | |
| - ``point_size`` : float, The point size in pixels. Defaults to 1px. | |
| Note | |
| ---- | |
| The valid keys for ``viewer_flags`` are as follows: | |
| - ``rotate``: `bool`, If `True`, the scene's camera will rotate | |
| about an axis. Defaults to `False`. | |
| - ``rotate_rate``: `float`, The rate of rotation in radians per second. | |
| Defaults to `PI / 3.0`. | |
| - ``rotate_axis``: `(3,) float`, The axis in world coordinates to rotate | |
| about. Defaults to ``[0,0,1]``. | |
| - ``view_center``: `(3,) float`, The position to rotate the scene about. | |
| Defaults to the scene's centroid. | |
| - ``use_raymond_lighting``: `bool`, If `True`, an additional set of three | |
| directional lights that move with the camera will be added to the scene. | |
| Defaults to `False`. | |
| - ``use_direct_lighting``: `bool`, If `True`, an additional directional | |
| light that moves with the camera and points out of it will be added to | |
| the scene. Defaults to `False`. | |
| - ``lighting_intensity``: `float`, The overall intensity of the | |
| viewer's additional lights (when they're in use). Defaults to 3.0. | |
| - ``use_perspective_cam``: `bool`, If `True`, a perspective camera will | |
| be used. Otherwise, an orthographic camera is used. Defaults to `True`. | |
| - ``save_directory``: `str`, A directory to open the file dialogs in. | |
| Defaults to `None`. | |
| - ``window_title``: `str`, A title for the viewer's application window. | |
| Defaults to `"Scene Viewer"`. | |
| - ``refresh_rate``: `float`, A refresh rate for rendering, in Hertz. | |
| Defaults to `30.0`. | |
| - ``fullscreen``: `bool`, Whether to make viewer fullscreen. | |
| Defaults to `False`. | |
| - ``show_world_axis``: `bool`, Whether to show the world axis. | |
| Defaults to `False`. | |
| - ``show_mesh_axes``: `bool`, Whether to show the individual mesh axes. | |
| Defaults to `False`. | |
| - ``caption``: `list of dict`, Text caption(s) to display on the viewer. | |
| Defaults to `None`. | |
| Note | |
| ---- | |
| Animation can be accomplished by running the viewer with ``run_in_thread`` | |
| enabled. Then, just run a loop in your main thread, updating the scene as | |
| needed. Before updating the scene, be sure to acquire the | |
| :attr:`.Viewer.render_lock`, and release it when your update is done. | |
| """ | |
| def __init__(self, scene, viewport_size=None, | |
| render_flags=None, viewer_flags=None, | |
| registered_keys=None, run_in_thread=False, | |
| auto_start=True, | |
| **kwargs): | |
| ####################################################################### | |
| # Save attributes and flags | |
| ####################################################################### | |
| if viewport_size is None: | |
| viewport_size = (640, 480) | |
| self._scene = scene | |
| self._viewport_size = viewport_size | |
| self._render_lock = RLock() | |
| self._is_active = False | |
| self._should_close = False | |
| self._run_in_thread = run_in_thread | |
| self._auto_start = auto_start | |
| self._default_render_flags = { | |
| 'flip_wireframe': False, | |
| 'all_wireframe': False, | |
| 'all_solid': False, | |
| 'shadows': False, | |
| 'vertex_normals': False, | |
| 'face_normals': False, | |
| 'cull_faces': True, | |
| 'point_size': 1.0, | |
| } | |
| self._default_viewer_flags = { | |
| 'mouse_pressed': False, | |
| 'rotate': False, | |
| 'rotate_rate': np.pi / 3.0, | |
| 'rotate_axis': np.array([0.0, 0.0, 1.0]), | |
| 'view_center': None, | |
| 'record': False, | |
| 'use_raymond_lighting': False, | |
| 'use_direct_lighting': False, | |
| 'lighting_intensity': 3.0, | |
| 'use_perspective_cam': True, | |
| 'save_directory': None, | |
| 'window_title': 'Scene Viewer', | |
| 'refresh_rate': 30.0, | |
| 'fullscreen': False, | |
| 'show_world_axis': False, | |
| 'show_mesh_axes': False, | |
| 'caption': None | |
| } | |
| self._render_flags = self._default_render_flags.copy() | |
| self._viewer_flags = self._default_viewer_flags.copy() | |
| self._viewer_flags['rotate_axis'] = ( | |
| self._default_viewer_flags['rotate_axis'].copy() | |
| ) | |
| if render_flags is not None: | |
| self._render_flags.update(render_flags) | |
| if viewer_flags is not None: | |
| self._viewer_flags.update(viewer_flags) | |
| for key in kwargs: | |
| if key in self.render_flags: | |
| self._render_flags[key] = kwargs[key] | |
| elif key in self.viewer_flags: | |
| self._viewer_flags[key] = kwargs[key] | |
| # TODO MAC OS BUG FOR SHADOWS | |
| if sys.platform == 'darwin': | |
| self._render_flags['shadows'] = False | |
| self._registered_keys = {} | |
| if registered_keys is not None: | |
| self._registered_keys = { | |
| ord(k.lower()): registered_keys[k] for k in registered_keys | |
| } | |
| ####################################################################### | |
| # Save internal settings | |
| ####################################################################### | |
| # Set up caption stuff | |
| self._message_text = None | |
| self._ticks_till_fade = 2.0 / 3.0 * self.viewer_flags['refresh_rate'] | |
| self._message_opac = 1.0 + self._ticks_till_fade | |
| # Set up raymond lights and direct lights | |
| self._raymond_lights = self._create_raymond_lights() | |
| self._direct_light = self._create_direct_light() | |
| # Set up axes | |
| self._axes = {} | |
| self._axis_mesh = Mesh.from_trimesh( | |
| trimesh.creation.axis(origin_size=0.1, axis_radius=0.05, | |
| axis_length=1.0), smooth=False) | |
| if self.viewer_flags['show_world_axis']: | |
| self._set_axes(world=self.viewer_flags['show_world_axis'], | |
| mesh=self.viewer_flags['show_mesh_axes']) | |
| ####################################################################### | |
| # Set up camera node | |
| ####################################################################### | |
| self._camera_node = None | |
| self._prior_main_camera_node = None | |
| self._default_camera_pose = None | |
| self._default_persp_cam = None | |
| self._default_orth_cam = None | |
| self._trackball = None | |
| self._saved_frames = [] | |
| # Extract main camera from scene and set up our mirrored copy | |
| znear = None | |
| zfar = None | |
| if scene.main_camera_node is not None: | |
| n = scene.main_camera_node | |
| camera = copy.copy(n.camera) | |
| if isinstance(camera, (PerspectiveCamera, IntrinsicsCamera)): | |
| self._default_persp_cam = camera | |
| znear = camera.znear | |
| zfar = camera.zfar | |
| elif isinstance(camera, OrthographicCamera): | |
| self._default_orth_cam = camera | |
| znear = camera.znear | |
| zfar = camera.zfar | |
| self._default_camera_pose = scene.get_pose(scene.main_camera_node) | |
| self._prior_main_camera_node = n | |
| # Set defaults as needed | |
| if zfar is None: | |
| zfar = max(scene.scale * 10.0, DEFAULT_Z_FAR) | |
| if znear is None or znear == 0: | |
| if scene.scale == 0: | |
| znear = DEFAULT_Z_NEAR | |
| else: | |
| znear = min(scene.scale / 10.0, DEFAULT_Z_NEAR) | |
| if self._default_persp_cam is None: | |
| self._default_persp_cam = PerspectiveCamera( | |
| yfov=np.pi / 3.0, znear=znear, zfar=zfar | |
| ) | |
| if self._default_orth_cam is None: | |
| xmag = ymag = scene.scale | |
| if scene.scale == 0: | |
| xmag = ymag = 1.0 | |
| self._default_orth_cam = OrthographicCamera( | |
| xmag=xmag, ymag=ymag, | |
| znear=znear, | |
| zfar=zfar | |
| ) | |
| if self._default_camera_pose is None: | |
| self._default_camera_pose = self._compute_initial_camera_pose() | |
| # Pick camera | |
| if self.viewer_flags['use_perspective_cam']: | |
| camera = self._default_persp_cam | |
| else: | |
| camera = self._default_orth_cam | |
| self._camera_node = Node( | |
| matrix=self._default_camera_pose, camera=camera | |
| ) | |
| scene.add_node(self._camera_node) | |
| scene.main_camera_node = self._camera_node | |
| self._reset_view() | |
| ####################################################################### | |
| # Initialize OpenGL context and renderer | |
| ####################################################################### | |
| self._renderer = Renderer( | |
| self._viewport_size[0], self._viewport_size[1], | |
| self.render_flags['point_size'] | |
| ) | |
| self._is_active = True | |
| if self.run_in_thread: | |
| self._thread = Thread(target=self._init_and_start_app) | |
| self._thread.start() | |
| else: | |
| if auto_start: | |
| self._init_and_start_app() | |
| def start(self): | |
| self._init_and_start_app() | |
| def scene(self): | |
| """:class:`.Scene` : The scene being visualized. | |
| """ | |
| return self._scene | |
| def viewport_size(self): | |
| """(2,) int : The width and height of the viewing window. | |
| """ | |
| return self._viewport_size | |
| def render_lock(self): | |
| """:class:`threading.RLock` : If acquired, prevents the viewer from | |
| rendering until released. | |
| Run :meth:`.Viewer.render_lock.acquire` before making updates to | |
| the scene in a different thread, and run | |
| :meth:`.Viewer.render_lock.release` once you're done to let the viewer | |
| continue. | |
| """ | |
| return self._render_lock | |
| def is_active(self): | |
| """bool : `True` if the viewer is active, or `False` if it has | |
| been closed. | |
| """ | |
| return self._is_active | |
| def run_in_thread(self): | |
| """bool : Whether the viewer was run in a separate thread. | |
| """ | |
| return self._run_in_thread | |
| def render_flags(self): | |
| """dict : Flags for controlling the renderer's behavior. | |
| - ``flip_wireframe``: `bool`, If `True`, all objects will have their | |
| wireframe modes flipped from what their material indicates. | |
| Defaults to `False`. | |
| - ``all_wireframe``: `bool`, If `True`, all objects will be rendered | |
| in wireframe mode. Defaults to `False`. | |
| - ``all_solid``: `bool`, If `True`, all objects will be rendered in | |
| solid mode. Defaults to `False`. | |
| - ``shadows``: `bool`, If `True`, shadows will be rendered. | |
| Defaults to `False`. | |
| - ``vertex_normals``: `bool`, If `True`, vertex normals will be | |
| rendered as blue lines. Defaults to `False`. | |
| - ``face_normals``: `bool`, If `True`, face normals will be rendered as | |
| blue lines. Defaults to `False`. | |
| - ``cull_faces``: `bool`, If `True`, backfaces will be culled. | |
| Defaults to `True`. | |
| - ``point_size`` : float, The point size in pixels. Defaults to 1px. | |
| """ | |
| return self._render_flags | |
| def render_flags(self, value): | |
| self._render_flags = value | |
| def viewer_flags(self): | |
| """dict : Flags for controlling the viewer's behavior. | |
| The valid keys for ``viewer_flags`` are as follows: | |
| - ``rotate``: `bool`, If `True`, the scene's camera will rotate | |
| about an axis. Defaults to `False`. | |
| - ``rotate_rate``: `float`, The rate of rotation in radians per second. | |
| Defaults to `PI / 3.0`. | |
| - ``rotate_axis``: `(3,) float`, The axis in world coordinates to | |
| rotate about. Defaults to ``[0,0,1]``. | |
| - ``view_center``: `(3,) float`, The position to rotate the scene | |
| about. Defaults to the scene's centroid. | |
| - ``use_raymond_lighting``: `bool`, If `True`, an additional set of | |
| three directional lights that move with the camera will be added to | |
| the scene. Defaults to `False`. | |
| - ``use_direct_lighting``: `bool`, If `True`, an additional directional | |
| light that moves with the camera and points out of it will be | |
| added to the scene. Defaults to `False`. | |
| - ``lighting_intensity``: `float`, The overall intensity of the | |
| viewer's additional lights (when they're in use). Defaults to 3.0. | |
| - ``use_perspective_cam``: `bool`, If `True`, a perspective camera will | |
| be used. Otherwise, an orthographic camera is used. Defaults to | |
| `True`. | |
| - ``save_directory``: `str`, A directory to open the file dialogs in. | |
| Defaults to `None`. | |
| - ``window_title``: `str`, A title for the viewer's application window. | |
| Defaults to `"Scene Viewer"`. | |
| - ``refresh_rate``: `float`, A refresh rate for rendering, in Hertz. | |
| Defaults to `30.0`. | |
| - ``fullscreen``: `bool`, Whether to make viewer fullscreen. | |
| Defaults to `False`. | |
| - ``show_world_axis``: `bool`, Whether to show the world axis. | |
| Defaults to `False`. | |
| - ``show_mesh_axes``: `bool`, Whether to show the individual mesh axes. | |
| Defaults to `False`. | |
| - ``caption``: `list of dict`, Text caption(s) to display on | |
| the viewer. Defaults to `None`. | |
| """ | |
| return self._viewer_flags | |
| def viewer_flags(self, value): | |
| self._viewer_flags = value | |
| def registered_keys(self): | |
| """dict : Map from ASCII key character to a handler function. | |
| This is a map from ASCII key characters to tuples containing: | |
| - A function to be called whenever the key is pressed, | |
| whose first argument will be the viewer itself. | |
| - (Optionally) A list of additional positional arguments | |
| to be passed to the function. | |
| - (Optionally) A dict of keyword arguments to be passed | |
| to the function. | |
| """ | |
| return self._registered_keys | |
| def registered_keys(self, value): | |
| self._registered_keys = value | |
| def close_external(self): | |
| """Close the viewer from another thread. | |
| This function will wait for the actual close, so you immediately | |
| manipulate the scene afterwards. | |
| """ | |
| self._should_close = True | |
| while self.is_active: | |
| time.sleep(1.0 / self.viewer_flags['refresh_rate']) | |
| def save_gif(self, filename=None): | |
| """Save the stored GIF frames to a file. | |
| To use this asynchronously, run the viewer with the ``record`` | |
| flag and the ``run_in_thread`` flags set. | |
| Kill the viewer after your desired time with | |
| :meth:`.Viewer.close_external`, and then call :meth:`.Viewer.save_gif`. | |
| Parameters | |
| ---------- | |
| filename : str | |
| The file to save the GIF to. If not specified, | |
| a file dialog will be opened to ask the user where | |
| to save the GIF file. | |
| """ | |
| if filename is None: | |
| filename = self._get_save_filename(['gif', 'all']) | |
| if filename is not None: | |
| self.viewer_flags['save_directory'] = os.path.dirname(filename) | |
| imageio.mimwrite(filename, self._saved_frames, | |
| fps=self.viewer_flags['refresh_rate'], | |
| palettesize=128, subrectangles=True) | |
| self._saved_frames = [] | |
| def on_close(self): | |
| """Exit the event loop when the window is closed. | |
| """ | |
| # Remove our camera and restore the prior one | |
| if self._camera_node is not None: | |
| self.scene.remove_node(self._camera_node) | |
| if self._prior_main_camera_node is not None: | |
| self.scene.main_camera_node = self._prior_main_camera_node | |
| # Delete any lighting nodes that we've attached | |
| if self.viewer_flags['use_raymond_lighting']: | |
| for n in self._raymond_lights: | |
| if self.scene.has_node(n): | |
| self.scene.remove_node(n) | |
| if self.viewer_flags['use_direct_lighting']: | |
| if self.scene.has_node(self._direct_light): | |
| self.scene.remove_node(self._direct_light) | |
| # Delete any axis nodes that we've attached | |
| self._remove_axes() | |
| # Delete renderer | |
| if self._renderer is not None: | |
| self._renderer.delete() | |
| self._renderer = None | |
| # Force clean-up of OpenGL context data | |
| try: | |
| OpenGL.contextdata.cleanupContext() | |
| self.close() | |
| except Exception: | |
| pass | |
| finally: | |
| self._is_active = False | |
| super(Viewer, self).on_close() | |
| pyglet.app.exit() | |
| def on_draw(self): | |
| """Redraw the scene into the viewing window. | |
| """ | |
| if self._renderer is None: | |
| return | |
| if self.run_in_thread or not self._auto_start: | |
| self.render_lock.acquire() | |
| # Make OpenGL context current | |
| self.switch_to() | |
| # Render the scene | |
| self.clear() | |
| self._render() | |
| if self._message_text is not None: | |
| self._renderer.render_text( | |
| self._message_text, | |
| self.viewport_size[0] - TEXT_PADDING, | |
| TEXT_PADDING, | |
| font_pt=20, | |
| color=np.array([0.1, 0.7, 0.2, | |
| np.clip(self._message_opac, 0.0, 1.0)]), | |
| align=TextAlign.BOTTOM_RIGHT | |
| ) | |
| if self.viewer_flags['caption'] is not None: | |
| for caption in self.viewer_flags['caption']: | |
| xpos, ypos = self._location_to_x_y(caption['location']) | |
| self._renderer.render_text( | |
| caption['text'], | |
| xpos, | |
| ypos, | |
| font_name=caption['font_name'], | |
| font_pt=caption['font_pt'], | |
| color=caption['color'], | |
| scale=caption['scale'], | |
| align=caption['location'] | |
| ) | |
| if self.run_in_thread or not self._auto_start: | |
| self.render_lock.release() | |
| def on_resize(self, width, height): | |
| """Resize the camera and trackball when the window is resized. | |
| """ | |
| if self._renderer is None: | |
| return | |
| self._viewport_size = (width, height) | |
| self._trackball.resize(self._viewport_size) | |
| self._renderer.viewport_width = self._viewport_size[0] | |
| self._renderer.viewport_height = self._viewport_size[1] | |
| self.on_draw() | |
| def on_mouse_press(self, x, y, buttons, modifiers): | |
| """Record an initial mouse press. | |
| """ | |
| self._trackball.set_state(Trackball.STATE_ROTATE) | |
| if (buttons == pyglet.window.mouse.LEFT): | |
| ctrl = (modifiers & pyglet.window.key.MOD_CTRL) | |
| shift = (modifiers & pyglet.window.key.MOD_SHIFT) | |
| if (ctrl and shift): | |
| self._trackball.set_state(Trackball.STATE_ZOOM) | |
| elif ctrl: | |
| self._trackball.set_state(Trackball.STATE_ROLL) | |
| elif shift: | |
| self._trackball.set_state(Trackball.STATE_PAN) | |
| elif (buttons == pyglet.window.mouse.MIDDLE): | |
| self._trackball.set_state(Trackball.STATE_PAN) | |
| elif (buttons == pyglet.window.mouse.RIGHT): | |
| self._trackball.set_state(Trackball.STATE_ZOOM) | |
| self._trackball.down(np.array([x, y])) | |
| # Stop animating while using the mouse | |
| self.viewer_flags['mouse_pressed'] = True | |
| def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): | |
| """Record a mouse drag. | |
| """ | |
| self._trackball.drag(np.array([x, y])) | |
| def on_mouse_release(self, x, y, button, modifiers): | |
| """Record a mouse release. | |
| """ | |
| self.viewer_flags['mouse_pressed'] = False | |
| def on_mouse_scroll(self, x, y, dx, dy): | |
| """Record a mouse scroll. | |
| """ | |
| if self.viewer_flags['use_perspective_cam']: | |
| self._trackball.scroll(dy) | |
| else: | |
| spfc = 0.95 | |
| spbc = 1.0 / 0.95 | |
| sf = 1.0 | |
| if dy > 0: | |
| sf = spfc * dy | |
| elif dy < 0: | |
| sf = - spbc * dy | |
| c = self._camera_node.camera | |
| xmag = max(c.xmag * sf, 1e-8) | |
| ymag = max(c.ymag * sf, 1e-8 * c.ymag / c.xmag) | |
| c.xmag = xmag | |
| c.ymag = ymag | |
| def on_key_press(self, symbol, modifiers): | |
| """Record a key press. | |
| """ | |
| # First, check for registered key callbacks | |
| if symbol in self.registered_keys: | |
| tup = self.registered_keys[symbol] | |
| callback = None | |
| args = [] | |
| kwargs = {} | |
| if not isinstance(tup, (list, tuple, np.ndarray)): | |
| callback = tup | |
| else: | |
| callback = tup[0] | |
| if len(tup) == 2: | |
| args = tup[1] | |
| if len(tup) == 3: | |
| kwargs = tup[2] | |
| callback(self, *args, **kwargs) | |
| return | |
| # Otherwise, use default key functions | |
| # A causes the frame to rotate | |
| self._message_text = None | |
| if symbol == pyglet.window.key.A: | |
| self.viewer_flags['rotate'] = not self.viewer_flags['rotate'] | |
| if self.viewer_flags['rotate']: | |
| self._message_text = 'Rotation On' | |
| else: | |
| self._message_text = 'Rotation Off' | |
| # C toggles backface culling | |
| elif symbol == pyglet.window.key.C: | |
| self.render_flags['cull_faces'] = ( | |
| not self.render_flags['cull_faces'] | |
| ) | |
| if self.render_flags['cull_faces']: | |
| self._message_text = 'Cull Faces On' | |
| else: | |
| self._message_text = 'Cull Faces Off' | |
| # F toggles face normals | |
| elif symbol == pyglet.window.key.F: | |
| self.viewer_flags['fullscreen'] = ( | |
| not self.viewer_flags['fullscreen'] | |
| ) | |
| self.set_fullscreen(self.viewer_flags['fullscreen']) | |
| self.activate() | |
| if self.viewer_flags['fullscreen']: | |
| self._message_text = 'Fullscreen On' | |
| else: | |
| self._message_text = 'Fullscreen Off' | |
| # S toggles shadows | |
| elif symbol == pyglet.window.key.H and sys.platform != 'darwin': | |
| self.render_flags['shadows'] = not self.render_flags['shadows'] | |
| if self.render_flags['shadows']: | |
| self._message_text = 'Shadows On' | |
| else: | |
| self._message_text = 'Shadows Off' | |
| elif symbol == pyglet.window.key.I: | |
| if (self.viewer_flags['show_world_axis'] and not | |
| self.viewer_flags['show_mesh_axes']): | |
| self.viewer_flags['show_world_axis'] = False | |
| self.viewer_flags['show_mesh_axes'] = True | |
| self._set_axes(False, True) | |
| self._message_text = 'Mesh Axes On' | |
| elif (not self.viewer_flags['show_world_axis'] and | |
| self.viewer_flags['show_mesh_axes']): | |
| self.viewer_flags['show_world_axis'] = True | |
| self.viewer_flags['show_mesh_axes'] = True | |
| self._set_axes(True, True) | |
| self._message_text = 'All Axes On' | |
| elif (self.viewer_flags['show_world_axis'] and | |
| self.viewer_flags['show_mesh_axes']): | |
| self.viewer_flags['show_world_axis'] = False | |
| self.viewer_flags['show_mesh_axes'] = False | |
| self._set_axes(False, False) | |
| self._message_text = 'All Axes Off' | |
| else: | |
| self.viewer_flags['show_world_axis'] = True | |
| self.viewer_flags['show_mesh_axes'] = False | |
| self._set_axes(True, False) | |
| self._message_text = 'World Axis On' | |
| # L toggles the lighting mode | |
| elif symbol == pyglet.window.key.L: | |
| if self.viewer_flags['use_raymond_lighting']: | |
| self.viewer_flags['use_raymond_lighting'] = False | |
| self.viewer_flags['use_direct_lighting'] = True | |
| self._message_text = 'Direct Lighting' | |
| elif self.viewer_flags['use_direct_lighting']: | |
| self.viewer_flags['use_raymond_lighting'] = False | |
| self.viewer_flags['use_direct_lighting'] = False | |
| self._message_text = 'Default Lighting' | |
| else: | |
| self.viewer_flags['use_raymond_lighting'] = True | |
| self.viewer_flags['use_direct_lighting'] = False | |
| self._message_text = 'Raymond Lighting' | |
| # M toggles face normals | |
| elif symbol == pyglet.window.key.M: | |
| self.render_flags['face_normals'] = ( | |
| not self.render_flags['face_normals'] | |
| ) | |
| if self.render_flags['face_normals']: | |
| self._message_text = 'Face Normals On' | |
| else: | |
| self._message_text = 'Face Normals Off' | |
| # N toggles vertex normals | |
| elif symbol == pyglet.window.key.N: | |
| self.render_flags['vertex_normals'] = ( | |
| not self.render_flags['vertex_normals'] | |
| ) | |
| if self.render_flags['vertex_normals']: | |
| self._message_text = 'Vert Normals On' | |
| else: | |
| self._message_text = 'Vert Normals Off' | |
| # O toggles orthographic camera mode | |
| elif symbol == pyglet.window.key.O: | |
| self.viewer_flags['use_perspective_cam'] = ( | |
| not self.viewer_flags['use_perspective_cam'] | |
| ) | |
| if self.viewer_flags['use_perspective_cam']: | |
| camera = self._default_persp_cam | |
| self._message_text = 'Perspective View' | |
| else: | |
| camera = self._default_orth_cam | |
| self._message_text = 'Orthographic View' | |
| cam_pose = self._camera_node.matrix.copy() | |
| cam_node = Node(matrix=cam_pose, camera=camera) | |
| self.scene.remove_node(self._camera_node) | |
| self.scene.add_node(cam_node) | |
| self.scene.main_camera_node = cam_node | |
| self._camera_node = cam_node | |
| # Q quits the viewer | |
| elif symbol == pyglet.window.key.Q: | |
| self.on_close() | |
| # R starts recording frames | |
| elif symbol == pyglet.window.key.R: | |
| if self.viewer_flags['record']: | |
| self.save_gif() | |
| self.set_caption(self.viewer_flags['window_title']) | |
| else: | |
| self.set_caption( | |
| '{} (RECORDING)'.format(self.viewer_flags['window_title']) | |
| ) | |
| self.viewer_flags['record'] = not self.viewer_flags['record'] | |
| # S saves the current frame as an image | |
| elif symbol == pyglet.window.key.S: | |
| self._save_image() | |
| # W toggles through wireframe modes | |
| elif symbol == pyglet.window.key.W: | |
| if self.render_flags['flip_wireframe']: | |
| self.render_flags['flip_wireframe'] = False | |
| self.render_flags['all_wireframe'] = True | |
| self.render_flags['all_solid'] = False | |
| self._message_text = 'All Wireframe' | |
| elif self.render_flags['all_wireframe']: | |
| self.render_flags['flip_wireframe'] = False | |
| self.render_flags['all_wireframe'] = False | |
| self.render_flags['all_solid'] = True | |
| self._message_text = 'All Solid' | |
| elif self.render_flags['all_solid']: | |
| self.render_flags['flip_wireframe'] = False | |
| self.render_flags['all_wireframe'] = False | |
| self.render_flags['all_solid'] = False | |
| self._message_text = 'Default Wireframe' | |
| else: | |
| self.render_flags['flip_wireframe'] = True | |
| self.render_flags['all_wireframe'] = False | |
| self.render_flags['all_solid'] = False | |
| self._message_text = 'Flip Wireframe' | |
| # Z resets the camera viewpoint | |
| elif symbol == pyglet.window.key.Z: | |
| self._reset_view() | |
| if self._message_text is not None: | |
| self._message_opac = 1.0 + self._ticks_till_fade | |
| def _time_event(dt, self): | |
| """The timer callback. | |
| """ | |
| # Don't run old dead events after we've already closed | |
| if not self._is_active: | |
| return | |
| if self.viewer_flags['record']: | |
| self._record() | |
| if (self.viewer_flags['rotate'] and not | |
| self.viewer_flags['mouse_pressed']): | |
| self._rotate() | |
| # Manage message opacity | |
| if self._message_text is not None: | |
| if self._message_opac > 1.0: | |
| self._message_opac -= 1.0 | |
| else: | |
| self._message_opac *= 0.90 | |
| if self._message_opac < 0.05: | |
| self._message_opac = 1.0 + self._ticks_till_fade | |
| self._message_text = None | |
| if self._should_close: | |
| self.on_close() | |
| else: | |
| self.on_draw() | |
| def _reset_view(self): | |
| """Reset the view to a good initial state. | |
| The view is initially along the positive x-axis at a | |
| sufficient distance from the scene. | |
| """ | |
| scale = self.scene.scale | |
| if scale == 0.0: | |
| scale = DEFAULT_SCENE_SCALE | |
| centroid = self.scene.centroid | |
| if self.viewer_flags['view_center'] is not None: | |
| centroid = self.viewer_flags['view_center'] | |
| self._camera_node.matrix = self._default_camera_pose | |
| self._trackball = Trackball( | |
| self._default_camera_pose, self.viewport_size, scale, centroid | |
| ) | |
| def _get_save_filename(self, file_exts): | |
| file_types = { | |
| 'png': ('png files', '*.png'), | |
| 'jpg': ('jpeg files', '*.jpg'), | |
| 'gif': ('gif files', '*.gif'), | |
| 'all': ('all files', '*'), | |
| } | |
| filetypes = [file_types[x] for x in file_exts] | |
| try: | |
| root = Tk() | |
| save_dir = self.viewer_flags['save_directory'] | |
| if save_dir is None: | |
| save_dir = os.getcwd() | |
| filename = filedialog.asksaveasfilename( | |
| initialdir=save_dir, title='Select file save location', | |
| filetypes=filetypes | |
| ) | |
| except Exception: | |
| return None | |
| root.destroy() | |
| if filename == (): | |
| return None | |
| return filename | |
| def _save_image(self): | |
| filename = self._get_save_filename(['png', 'jpg', 'gif', 'all']) | |
| if filename is not None: | |
| self.viewer_flags['save_directory'] = os.path.dirname(filename) | |
| imageio.imwrite(filename, self._renderer.read_color_buf()) | |
| def _record(self): | |
| """Save another frame for the GIF. | |
| """ | |
| data = self._renderer.read_color_buf() | |
| if not np.all(data == 0.0): | |
| self._saved_frames.append(data) | |
| def _rotate(self): | |
| """Animate the scene by rotating the camera. | |
| """ | |
| az = (self.viewer_flags['rotate_rate'] / | |
| self.viewer_flags['refresh_rate']) | |
| self._trackball.rotate(az, self.viewer_flags['rotate_axis']) | |
| def _render(self): | |
| """Render the scene into the framebuffer and flip. | |
| """ | |
| scene = self.scene | |
| self._camera_node.matrix = self._trackball.pose.copy() | |
| # Set lighting | |
| vli = self.viewer_flags['lighting_intensity'] | |
| if self.viewer_flags['use_raymond_lighting']: | |
| for n in self._raymond_lights: | |
| n.light.intensity = vli / 3.0 | |
| if not self.scene.has_node(n): | |
| scene.add_node(n, parent_node=self._camera_node) | |
| else: | |
| self._direct_light.light.intensity = vli | |
| for n in self._raymond_lights: | |
| if self.scene.has_node(n): | |
| self.scene.remove_node(n) | |
| if self.viewer_flags['use_direct_lighting']: | |
| if not self.scene.has_node(self._direct_light): | |
| scene.add_node( | |
| self._direct_light, parent_node=self._camera_node | |
| ) | |
| elif self.scene.has_node(self._direct_light): | |
| self.scene.remove_node(self._direct_light) | |
| flags = RenderFlags.NONE | |
| if self.render_flags['flip_wireframe']: | |
| flags |= RenderFlags.FLIP_WIREFRAME | |
| elif self.render_flags['all_wireframe']: | |
| flags |= RenderFlags.ALL_WIREFRAME | |
| elif self.render_flags['all_solid']: | |
| flags |= RenderFlags.ALL_SOLID | |
| if self.render_flags['shadows']: | |
| flags |= RenderFlags.SHADOWS_DIRECTIONAL | RenderFlags.SHADOWS_SPOT | |
| if self.render_flags['vertex_normals']: | |
| flags |= RenderFlags.VERTEX_NORMALS | |
| if self.render_flags['face_normals']: | |
| flags |= RenderFlags.FACE_NORMALS | |
| if not self.render_flags['cull_faces']: | |
| flags |= RenderFlags.SKIP_CULL_FACES | |
| self._renderer.render(self.scene, flags) | |
| def _init_and_start_app(self): | |
| # Try multiple configs starting with target OpenGL version | |
| # and multisampling and removing these options if exception | |
| # Note: multisampling not available on all hardware | |
| from pyglet.gl import Config | |
| confs = [Config(sample_buffers=1, samples=4, | |
| depth_size=24, | |
| double_buffer=True, | |
| major_version=TARGET_OPEN_GL_MAJOR, | |
| minor_version=TARGET_OPEN_GL_MINOR), | |
| Config(depth_size=24, | |
| double_buffer=True, | |
| major_version=TARGET_OPEN_GL_MAJOR, | |
| minor_version=TARGET_OPEN_GL_MINOR), | |
| Config(sample_buffers=1, samples=4, | |
| depth_size=24, | |
| double_buffer=True, | |
| major_version=MIN_OPEN_GL_MAJOR, | |
| minor_version=MIN_OPEN_GL_MINOR), | |
| Config(depth_size=24, | |
| double_buffer=True, | |
| major_version=MIN_OPEN_GL_MAJOR, | |
| minor_version=MIN_OPEN_GL_MINOR)] | |
| for conf in confs: | |
| try: | |
| super(Viewer, self).__init__(config=conf, resizable=True, | |
| width=self._viewport_size[0], | |
| height=self._viewport_size[1]) | |
| break | |
| except pyglet.window.NoSuchConfigException: | |
| pass | |
| if not self.context: | |
| raise ValueError('Unable to initialize an OpenGL 3+ context') | |
| clock.schedule_interval( | |
| Viewer._time_event, 1.0 / self.viewer_flags['refresh_rate'], self | |
| ) | |
| self.switch_to() | |
| self.set_caption(self.viewer_flags['window_title']) | |
| pyglet.app.run() | |
| def _compute_initial_camera_pose(self): | |
| centroid = self.scene.centroid | |
| if self.viewer_flags['view_center'] is not None: | |
| centroid = self.viewer_flags['view_center'] | |
| scale = self.scene.scale | |
| if scale == 0.0: | |
| scale = DEFAULT_SCENE_SCALE | |
| s2 = 1.0 / np.sqrt(2.0) | |
| cp = np.eye(4) | |
| cp[:3,:3] = np.array([ | |
| [0.0, -s2, s2], | |
| [1.0, 0.0, 0.0], | |
| [0.0, s2, s2] | |
| ]) | |
| hfov = np.pi / 6.0 | |
| dist = scale / (2.0 * np.tan(hfov)) | |
| cp[:3,3] = dist * np.array([1.0, 0.0, 1.0]) + centroid | |
| return cp | |
| def _create_raymond_lights(self): | |
| thetas = np.pi * np.array([1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0]) | |
| phis = np.pi * np.array([0.0, 2.0 / 3.0, 4.0 / 3.0]) | |
| nodes = [] | |
| for phi, theta in zip(phis, thetas): | |
| xp = np.sin(theta) * np.cos(phi) | |
| yp = np.sin(theta) * np.sin(phi) | |
| zp = np.cos(theta) | |
| z = np.array([xp, yp, zp]) | |
| z = z / np.linalg.norm(z) | |
| x = np.array([-z[1], z[0], 0.0]) | |
| if np.linalg.norm(x) == 0: | |
| x = np.array([1.0, 0.0, 0.0]) | |
| x = x / np.linalg.norm(x) | |
| y = np.cross(z, x) | |
| matrix = np.eye(4) | |
| matrix[:3,:3] = np.c_[x,y,z] | |
| nodes.append(Node( | |
| light=DirectionalLight(color=np.ones(3), intensity=1.0), | |
| matrix=matrix | |
| )) | |
| return nodes | |
| def _create_direct_light(self): | |
| light = DirectionalLight(color=np.ones(3), intensity=1.0) | |
| n = Node(light=light, matrix=np.eye(4)) | |
| return n | |
| def _set_axes(self, world, mesh): | |
| scale = self.scene.scale | |
| if world: | |
| if 'scene' not in self._axes: | |
| n = Node(mesh=self._axis_mesh, scale=np.ones(3) * scale * 0.3) | |
| self.scene.add_node(n) | |
| self._axes['scene'] = n | |
| else: | |
| if 'scene' in self._axes: | |
| self.scene.remove_node(self._axes['scene']) | |
| self._axes.pop('scene') | |
| if mesh: | |
| old_nodes = [] | |
| existing_axes = set([self._axes[k] for k in self._axes]) | |
| for node in self.scene.mesh_nodes: | |
| if node not in existing_axes: | |
| old_nodes.append(node) | |
| for node in old_nodes: | |
| if node in self._axes: | |
| continue | |
| n = Node( | |
| mesh=self._axis_mesh, | |
| scale=np.ones(3) * node.mesh.scale * 0.5 | |
| ) | |
| self.scene.add_node(n, parent_node=node) | |
| self._axes[node] = n | |
| else: | |
| to_remove = set() | |
| for main_node in self._axes: | |
| if main_node in self.scene.mesh_nodes: | |
| self.scene.remove_node(self._axes[main_node]) | |
| to_remove.add(main_node) | |
| for main_node in to_remove: | |
| self._axes.pop(main_node) | |
| def _remove_axes(self): | |
| for main_node in self._axes: | |
| axis_node = self._axes[main_node] | |
| self.scene.remove_node(axis_node) | |
| self._axes = {} | |
| def _location_to_x_y(self, location): | |
| if location == TextAlign.CENTER: | |
| return (self.viewport_size[0] / 2.0, self.viewport_size[1] / 2.0) | |
| elif location == TextAlign.CENTER_LEFT: | |
| return (TEXT_PADDING, self.viewport_size[1] / 2.0) | |
| elif location == TextAlign.CENTER_RIGHT: | |
| return (self.viewport_size[0] - TEXT_PADDING, | |
| self.viewport_size[1] / 2.0) | |
| elif location == TextAlign.BOTTOM_LEFT: | |
| return (TEXT_PADDING, TEXT_PADDING) | |
| elif location == TextAlign.BOTTOM_RIGHT: | |
| return (self.viewport_size[0] - TEXT_PADDING, TEXT_PADDING) | |
| elif location == TextAlign.BOTTOM_CENTER: | |
| return (self.viewport_size[0] / 2.0, TEXT_PADDING) | |
| elif location == TextAlign.TOP_LEFT: | |
| return (TEXT_PADDING, self.viewport_size[1] - TEXT_PADDING) | |
| elif location == TextAlign.TOP_RIGHT: | |
| return (self.viewport_size[0] - TEXT_PADDING, | |
| self.viewport_size[1] - TEXT_PADDING) | |
| elif location == TextAlign.TOP_CENTER: | |
| return (self.viewport_size[0] / 2.0, | |
| self.viewport_size[1] - TEXT_PADDING) | |
| __all__ = ['Viewer'] | |