#!/usr/bin/env python3 import textwrap __doc__ = textwrap.dedent( """ `meshviewer` is a program that allows you to display polygonal meshes produced by `mesh` package. Viewing a mesh on a local machine --------------------------------- The most straightforward use-case is viewing the mesh on the same machine where it is stored. To do this simply run ``` $ meshviewer view sphere.obj ``` This will create an interactive window with your mesh rendering. You can render more than one mesh in the same window by passing several paths to `view` command ``` $ meshviewer view sphere.obj cylinder.obj ``` This will arrange the subplots horizontally in a row. If you want a grid arrangement, you can specify the grid parameters explicitly ``` $ meshviewer view -nx 2 -ny 2 *.obj ``` Viewing a mesh from a remote machine ------------------------------------ It is also possible to view a mesh stored on a remote machine. To do this you need mesh to be installed on both the local and the remote machines. You start by opening an empty viewer window listening on a network port ``` (local) $ meshviewer open --port 3000 ``` To stream a shape to this viewer you have to either pick a port that is visible from the remote machine or by manually exposing the port when connecting. For example, through SSH port forwarding ``` (local) $ ssh -R 3000:127.0.0.1:3000 user@host ``` Then on a remote machine you use `view` command pointing to the locally forwarded port ``` (remote) $ meshviewer view -p 3000 sphere.obj ``` This should display the remote mesh on your local viewer. In case it does not it might be caused by the network connection being closed before the mesh could be sent. To work around this one can try increasing the timeout up to 1 second ``` (remote) $ meshviewer view -p 3000 --timeout 1 sphere.obj ``` To take a snapshot you should locally run a `snap` command ``` (local) $ meshviewer snap -p 3000 sphere.png ``` """) import argparse import logging import sys import time from psbody.mesh.mesh import Mesh from psbody.mesh.meshviewer import ( MESH_VIEWER_DEFAULT_TITLE, MESH_VIEWER_DEFAULT_SHAPE, MESH_VIEWER_DEFAULT_WIDTH, MESH_VIEWER_DEFAULT_HEIGHT, ZMQ_HOST, MeshViewerLocal, MeshViewerRemote) logging.basicConfig(level=logging.INFO) parser_root = argparse.ArgumentParser( add_help=False, description="View the polygonal meshes, locally and across the network", epilog=__doc__, formatter_class=argparse.RawTextHelpFormatter) subparsers = parser_root.add_subparsers(dest="command") subparsers.required = True parser_open = subparsers.add_parser("open", add_help=False) parser_open.add_argument( "-p", "--port", help="local port to listen for incoming commands", type=int) parser_view = subparsers.add_parser("view", add_help=False) parser_view.add_argument( "-h", "--host", help="remote host", metavar="HOSTNAME", type=str) parser_view.add_argument( "-p", "--port", help="remote port", type=int) parser_view.add_argument( "-ix", "--subwindow-index-horizontal", help="horizontal index of the target subwindow", metavar="INDEX", type=int) parser_view.add_argument( "-iy", "--subwindow-index-vertical", help="vertical index of the target subwindow", metavar="INDEX", type=int) parser_view.add_argument( "--timeout", help="wait for some time after sending the mesh to let it render", metavar="SECONDS", type=float, default=0.5) parser_view.add_argument( "filename", help="path to the mesh file", type=str, nargs="+") for parser in parser_open, parser_view: window_options = parser.add_argument_group("window options") window_options.add_argument( "-t", "--title", help="window title", type=str) window_options.add_argument( "-ww", "-wx", "--window-width", help="window width in pixels", metavar="PIXELS", type=int) window_options.add_argument( "-wh", "-wy", "--window-height", help="window height in pixels", metavar="PIXELS", type=int) window_options.add_argument( "-nx", "--subwindow-number-horizontal", help="number of horizontal subwindows", metavar="NUMBER", type=int) window_options.add_argument( "-ny", "--subwindow-number-vertical", help="number of vertical subwindows", metavar="NUMBER", type=int) parser_snap = subparsers.add_parser("snap", add_help=False) parser_snap.add_argument( "-h", "--host", help="remote host", type=str) parser_snap.add_argument( "-p", "--port", help="remote port", type=int, required=True) parser_snap.add_argument( "filename", help="path to the output snapshot", type=str) for p in parser_root, parser_open, parser_view, parser_snap: p.add_argument("--help", action="help") def dispatch_command(args): """ Performs a sanity check of the passed arguments and then dispatches the appropriate command. """ if args.command == "open": start_server(args) return if not args.port: client = start_local_client(args) else: client = start_remote_client(args) if args.command == "snap": take_snapshot(client, args) if args.command == "view": if args.port is not None: # Below is a list of contradicting settings: it futile to # try to change the parameters of a mesh viewer already # running on a remote machine. if args.title is not None: logging.warning( "--title is ignored when working with remote viewer") if args.window_width is not None: logging.warning( "--window-width is ignored when working with remote viewer") if args.window_height is not None: logging.warning( "--window-height is ignored when working with remote viewer") if args.subwindow_number_horizontal is not None: logging.warning( "--subwindow-number-horizontal is ignored when working " "with remote viewer") if args.subwindow_number_vertical is not None: logging.warning( "--subwindow-number-vertical is ignored when working " "with remote viewer") # This one is a bit different: while it should be # technically possible to stream the mesh in a specific # subwindow, we currently don't support that. if ( args.subwindow_index_horizontal is not None or args.subwindow_index_vertical is not None ): logging.warning( "unfortunately, drawing to a specific subwindow is not " "supported when working with remote viewer and the first " "subwindow is going to be used instead") if ( args.subwindow_index_horizontal is not None and args.subwindow_index_vertical is None ) or ( args.subwindow_index_horizontal is None and args.subwindow_index_vertical is not None ): logging.fatal( "you have to specify both horizontal " "and vertical subwindow incides") return if ( args.subwindow_index_horizontal is not None and args.subwindow_index_vertical is not None ): display_single_subwindow(client, args) else: display_multi_subwindows(client, args) # Basically, wait for send_pyobj() to actually send everything # before terminating. time.sleep(args.timeout) def start_server(args): """ Starts a meshviewer window on a local machine. This function opens a mesh viewer window that listens for command on a given port. """ server = MeshViewerRemote( titlebar=args.title or MESH_VIEWER_DEFAULT_TITLE, subwins_vert=args.subwindow_number_vertical or MESH_VIEWER_DEFAULT_SHAPE[1], subwins_horz=args.subwindow_number_horizontal or MESH_VIEWER_DEFAULT_SHAPE[0], width=args.window_width or MESH_VIEWER_DEFAULT_WIDTH, height=args.window_height or MESH_VIEWER_DEFAULT_HEIGHT, port=args.port) return server def start_local_client(args): """ Starts a local meshviewer not connected to anywhere. This function internally opens a mesh viewer window listening on a random port. """ client = MeshViewerLocal( titlebar=args.title or MESH_VIEWER_DEFAULT_TITLE, window_width=args.window_width or MESH_VIEWER_DEFAULT_WIDTH, window_height=args.window_height or MESH_VIEWER_DEFAULT_HEIGHT, shape=( args.subwindow_number_vertical or 1, args.subwindow_number_horizontal or len(args.filename), ), keepalive=True) return client def start_remote_client(args): """ Starts a meshviewer client connected to a remote machine. This function does not create a new window, but is necessary to stream the mesh to a remote viewer. """ client = MeshViewerLocal( host=args.host or ZMQ_HOST, port=args.port) return client def display_single_subwindow(client, args): """ Displays a single mesh in a given subwindow. """ ix = args.subwindow_index_horizontal iy = args.subwindow_index_vertical try: subwindow = client.get_subwindows()[iy][ix] except IndexError: logging.fatal( "cannot find subwindow ({}, {}). " "The current viewer shape is {}x{} subwindows, " "indexing is zero-based." .format(ix, iy, *client.shape)) return meshes = [Mesh(filename=filename) for filename in args.filename] subwindow.set_static_meshes(meshes) def display_multi_subwindows(client, args): """ Displays a list of meshes. One mesh per subwindow. """ grid = client.get_subwindows() subwindows = [ subwindow for row in grid for subwindow in row ] if len(subwindows) < len(args.filename): logging.warning( "cannot display {0} meshes in {1} subwindows. " "Taking the first {1}.".format( len(args.filename), len(subwindows))) for subwindow, filename in zip(subwindows, args.filename): mesh = Mesh(filename=filename) subwindow.set_static_meshes([mesh]) def take_snapshot(client, args): """ Take snapshot and dump it into a file. """ client.save_snapshot(args.filename) if __name__ == "__main__": args = parser_root.parse_args() dispatch_command(args) sys.exit(0)