| #!/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) | |