Team Ai
Apppublic

OpenMotionLab/MotionGPT

sourceHugging Facemitupdated 1y agoView on Hugging Face
118likes
viewer.py1161 linesDownload Raw Back to pyrender
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