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