#!/usr/bin/env python
# encoding: utf-8
# Copyright (c) 2012 Max Planck Society. All rights reserved.
# Created by Matthew Loper on 2012-05-11.
"""
Mesh visualization and related classes
--------------------------------------
This module contains the core visualization tools for meshes. The
backend used for visualization is OpenGL.
The module itself can be run like the following
.. code::
python -m psbody.mesh.meshviewer arguments
The following commands are used
* ``arguments=TEST_FOR_OPENGL`` a basic OpenGL support is run. This
is usually performed on a forked python process. In case OpenGL is
not supported, a `DummyClass`` mesh viewer is returned.
* ``arguments=title nb_x_axis nb_y_axis width height`` a new window is
created
.. autosummary::
MeshViewer
MeshViewers
MeshViewerLocal
test_for_opengl
"""
import copy
import logging
import multiprocessing
import os
import re
import subprocess
import sys
import tempfile
import time
import traceback
import numpy as np
from OpenGL import GL, GLU, GLUT
from OpenGL.arrays.vbo import VBO
from PIL import Image
import zmq
# if this file is processed/run as a python script/standalone, especially from the
# internal command
if __package__ is not None:
from .arcball import (
ArcBallT, Matrix3fT, Matrix4fT, Point2fT,
Matrix3fMulMatrix3f, Matrix3fSetRotationFromQuat4f,
Matrix4fSetRotationFromMatrix3f)
from .geometry.tri_normals import TriNormals
from .fonts import get_textureid_with_text
from .mesh import Mesh
# this block is below the previous one to make my linter happy
if __package__ is None:
print("this file cannot be executed as a standalone python module")
print("python -m psbody.mesh.%s arguments" % (os.path.splitext(os.path.basename(__file__))[0]))
sys.exit(-1)
# Default transport and host are such that we can listen for incoming
# network connections.
ZMQ_TRANSPORT = "tcp"
ZMQ_HOST = "0.0.0.0"
# The dynamic port range.
ZMQ_PORT_MIN = 49152
ZMQ_PORT_MAX = 65535
MESH_VIEWER_DEFAULT_TITLE = "Mesh Viewer"
MESH_VIEWER_DEFAULT_SHAPE = (1, 1)
MESH_VIEWER_DEFAULT_WIDTH = 1280
MESH_VIEWER_DEFAULT_HEIGHT = 960
def _run_self(args, stdin=None, stdout=None, stderr=None):
"""Executes this same script module with the given arguments (forking without subprocess dependencies)"""
return subprocess.Popen([sys.executable] +
['-m'] + ['%s.%s' % (__package__, os.path.splitext(os.path.basename(__file__))[0])] +
args,
stdin=stdin,
stdout=stdout, # if stdout is not None else subprocess.PIPE,
stderr=stderr)
def _test_for_opengl():
try:
# from OpenGL.GLUT import glutInit
GLUT.glutInit()
except Exception as e:
print(e, file=sys.stderr)
print('failure')
else:
print('success')
test_for_opengl_cached = None
def test_for_opengl():
"""Tests if opengl is supported.
.. note:: the result of the test is cached
"""
global test_for_opengl_cached
if test_for_opengl_cached is None:
with open(os.devnull) as dev_null, \
tempfile.TemporaryFile() as out, \
tempfile.TemporaryFile() as err:
p = _run_self(["TEST_FOR_OPENGL"],
stdin=dev_null,
stdout=out,
stderr=err)
p.wait()
out.seek(0)
err.seek(0)
line = ''.join(out.read().decode())
test_for_opengl_cached = 'success' in line
if not test_for_opengl_cached:
print('OpenGL test failed: ')
print('\tstdout:', line)
print('\tstderr:', '\n'.join(err.read().decode()))
return test_for_opengl_cached
class Dummy:
def __getattr__(self, name):
return Dummy()
def __call__(self, *args, **kwargs):
return Dummy()
def __getitem__(self, key):
return Dummy()
def __setitem__(self, key, value):
pass
def MeshViewer(titlebar='Mesh Viewer',
static_meshes=None,
static_lines=None,
uid=None,
autorecenter=True,
shape=(1, 1),
keepalive=True,
window_width=1280,
window_height=960,
snapshot_camera=None):
"""Allows visual inspection of geometric primitives.
Write-only Attributes:
:param titlebar: string printed in the window titlebar
:param static_meshes: list of Mesh objects to be displayed
:param static_lines: list of Lines objects to be displayed
.. note:: `static_meshes` is meant for Meshes that are updated infrequently,
`and dynamic_meshes` is for Meshes that are updated frequently
(same for `dynamic_lines` vs. `static_lines`).
They may be treated differently for performance reasons.
"""
if not test_for_opengl():
return Dummy()
mv = MeshViewerLocal(shape=(1, 1),
uid=uid,
titlebar=titlebar,
keepalive=keepalive,
window_width=window_width,
window_height=window_height)
result = mv.get_subwindows()[0][0]
result.snapshot_camera = snapshot_camera
if static_meshes:
result.static_meshes = static_meshes
if static_lines:
result.static_lines = static_lines
result.autorecenter = autorecenter
return result
def MeshViewers(shape=(1, 1),
titlebar="Mesh Viewers",
keepalive=True,
window_width=1280,
window_height=960):
"""Allows subplot-style inspection of primitives in multiple subwindows.
:param shape: a tuple indicating the number of vertical and horizontal windows requested
:param titlebar: the title appearing on the created window
Returns: a list of lists of MeshViewer objects: one per window requested.
"""
if not test_for_opengl():
return Dummy()
mv = MeshViewerLocal(shape=shape,
titlebar=titlebar,
uid=None,
keepalive=keepalive,
window_width=window_width,
window_height=window_height)
return mv.get_subwindows()
class MeshSubwindow:
def __init__(self, parent_window, which_window):
self.parent_window = parent_window
self.which_window = which_window
def set_dynamic_meshes(self, list_of_meshes, blocking=False):
self.parent_window.set_dynamic_meshes(list_of_meshes, blocking, self.which_window)
def set_static_meshes(self, list_of_meshes, blocking=False):
self.parent_window.set_static_meshes(list_of_meshes, blocking, self.which_window)
# list_of_model_names_and_parameters should be of form [{'name': scape_model_name, 'parameters': scape_model_parameters}]
# here scape_model_name is the filepath of the scape model.
def set_dynamic_models(self, list_of_model_names_and_parameters, blocking=False):
self.parent_window.set_dynamic_models(list_of_model_names_and_parameters, blocking, self.which_window)
def set_dynamic_lines(self, list_of_lines, blocking=False):
self.parent_window.set_dynamic_lines(list_of_lines, blocking, self.which_window)
def set_static_lines(self, list_of_lines, blocking=False):
self.parent_window.set_static_lines(list_of_lines, blocking=blocking, which_window=self.which_window)
def set_titlebar(self, titlebar, blocking=False):
self.parent_window.set_titlebar(titlebar, blocking, which_window=self.which_window)
def set_lighting_on(self, lighting_on, blocking=True):
self.parent_window.set_lighting_on(lighting_on, blocking=blocking, which_window=self.which_window)
def set_autorecenter(self, autorecenter, blocking=False):
self.parent_window.set_autorecenter(autorecenter, blocking=blocking, which_window=self.which_window)
def set_background_color(self, background_color, blocking=False):
self.parent_window.set_background_color(background_color, blocking=blocking, which_window=self.which_window)
def save_snapshot(self, path, blocking=False):
self.parent_window.save_snapshot(
path, blocking=blocking, which_window=self.which_window)
def get_event(self):
return self.parent_window.get_event()
def get_keypress(self):
return self.parent_window.get_keypress()['key']
def get_mouseclick(self):
return self.parent_window.get_mouseclick()
def close(self):
self.parent_window.p.terminate()
background_color = property(fset=set_background_color, doc="Background color, as 3-element numpy array where 0 <= color <= 1.0.")
dynamic_meshes = property(fset=set_dynamic_meshes, doc="List of meshes for dynamic display.")
static_meshes = property(fset=set_static_meshes, doc="List of meshes for static display.")
dynamic_models = property(fset=set_dynamic_models, doc="List of model names and parameters for dynamic display.")
dynamic_lines = property(fset=set_dynamic_lines, doc="List of Lines for dynamic display.")
static_lines = property(fset=set_static_lines, doc="List of Lines for static display.")
titlebar = property(fset=set_titlebar, doc="Titlebar string.")
lighting_on = property(fset=set_lighting_on, doc="Titlebar string.")
class MeshViewerSingle:
def __init__(self, x1_pct, y1_pct, width_pct, height_pct):
assert(width_pct <= 1)
assert(height_pct <= 1)
self.dynamic_meshes = []
self.static_meshes = []
self.dynamic_models = []
self.dynamic_lines = []
self.static_lines = []
self.lighting_on = True
self.scape_models = {}
self.x1_pct = x1_pct
self.y1_pct = y1_pct
self.width_pct = width_pct
self.height_pct = height_pct
self.autorecenter = True
def get_dimensions(self):
d = {}
d['window_width'] = GLUT.glutGet(GLUT.GLUT_WINDOW_WIDTH)
d['window_height'] = GLUT.glutGet(GLUT.GLUT_WINDOW_HEIGHT)
d['subwindow_width'] = self.width_pct * d['window_width']
d['subwindow_height'] = self.height_pct * d['window_height']
d['subwindow_origin_x'] = self.x1_pct * d['window_width']
d['subwindow_origin_y'] = self.y1_pct * d['window_height']
return d
def on_draw(self, transform, want_camera=False):
d = self.get_dimensions()
GL.glViewport(
int(d['subwindow_origin_x']),
int(d['subwindow_origin_y']),
int(d['subwindow_width']),
int(d['subwindow_height']))
GL.glMatrixMode(GL.GL_PROJECTION)
GL.glLoadIdentity()
fov_degrees = 45.
near = 1.0
far = 100.
ratio = float(d['subwindow_width']) / float(d['subwindow_height'])
if d['subwindow_width'] < d['subwindow_height']:
xt = np.tan(fov_degrees * np.pi / 180. / 2.0) * near
yt = xt / ratio
GL.glFrustum(-xt, xt, -yt, yt, near, far)
else:
GLU.gluPerspective(fov_degrees, ratio, near, far)
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glLoadIdentity()
GL.glLightModeli(GL.GL_LIGHT_MODEL_TWO_SIDE, GL.GL_TRUE)
GL.glTranslatef(0.0, 0.0, -6.0)
# GL.glTranslatef(0.0,0.0,-3.5)
GL.glPushMatrix()
GL.glMultMatrixf(transform)
GL.glColor3f(1.0, 0.75, 0.75)
if self.autorecenter:
camera = self.draw_primitives_recentered(want_camera=want_camera)
else:
if hasattr(self, 'current_center') and hasattr(self, 'current_scalefactor'):
camera = self.draw_primitives(scalefactor=self.current_scalefactor, center=self.current_center)
else:
camera = self.draw_primitives(want_camera=want_camera)
GL.glPopMatrix()
if want_camera:
return camera
def draw_primitives_recentered(self, want_camera=False):
return self.draw_primitives(recenter=True, want_camera=want_camera)
@staticmethod
def set_shaders(m):
VERTEX_SHADER = GL.shaders.compileShader("""void main() {
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}""", GL.GL_VERTEX_SHADER)
FRAGMENT_SHADER = GL.shaders.compileShader("""void main() {
gl_FragColor = vec4( 0, 1, 0, 1 );
}""", GL.GL_FRAGMENT_SHADER)
m.shaders = GL.shaders.compileProgram(VERTEX_SHADER, FRAGMENT_SHADER)
@staticmethod
def set_texture(m):
texture_data = np.array(m.texture_image, dtype='int8')
m.textureID = GL.glGenTextures(1)
GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1)
GL.glBindTexture(GL.GL_TEXTURE_2D, m.textureID)
GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGB, texture_data.shape[1], texture_data.shape[0], 0, GL.GL_BGR, GL.GL_UNSIGNED_BYTE, texture_data.flatten())
GL.glHint(GL.GL_GENERATE_MIPMAP_HINT, GL.GL_NICEST) # must be GL_FASTEST, GL.GL_NICEST or GL_DONT_CARE
GL.glGenerateMipmap(GL.GL_TEXTURE_2D)
@staticmethod
def draw_mesh(m, lighting_on):
# Supply vertices
GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
m.vbo['v'].bind()
GL.glVertexPointer(3, GL.GL_FLOAT, 0, m.vbo['v'])
m.vbo['v'].unbind()
# Supply normals
if 'vn' in m.vbo.keys():
GL.glEnableClientState(GL.GL_NORMAL_ARRAY)
m.vbo['vn'].bind()
GL.glNormalPointer(GL.GL_FLOAT, 0, m.vbo['vn'])
m.vbo['vn'].unbind()
else:
GL.glDisableClientState(GL.GL_NORMAL_ARRAY)
# Supply colors
if 'vc' in m.vbo.keys():
GL.glEnableClientState(GL.GL_COLOR_ARRAY)
m.vbo['vc'].bind()
GL.glColorPointer(3, GL.GL_FLOAT, 0, m.vbo['vc'])
m.vbo['vc'].unbind()
else:
GL.glDisableClientState(GL.GL_COLOR_ARRAY)
if ('vt' in m.vbo.keys()) and hasattr(m, 'textureID'):
GL.glEnable(GL.GL_TEXTURE_2D)
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
GL.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE)
GL.glBindTexture(GL.GL_TEXTURE_2D, m.textureID)
GL.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY)
m.vbo['vt'].bind()
GL.glTexCoordPointer(2, GL.GL_FLOAT, 0, m.vbo['vt'])
m.vbo['vt'].unbind()
else:
GL.glDisable(GL.GL_TEXTURE_2D)
GL.glDisableClientState(GL.GL_TEXTURE_COORD_ARRAY)
# Draw
if len(m.f) > 0:
# ie if it is triangulated
if lighting_on:
GL.glEnable(GL.GL_LIGHTING)
else:
GL.glDisable(GL.GL_LIGHTING)
GL.glDrawElementsui(GL.GL_TRIANGLES, np.arange(m.f.size, dtype=np.uint32))
else:
# not triangulated, so disable lighting
GL.glDisable(GL.GL_LIGHTING)
GL.glPointSize(2)
GL.glDrawElementsui(GL.GL_POINTS, np.arange(len(m.v), dtype=np.uint32))
if hasattr(m, 'v_to_text'):
GL.glEnable(GL.GL_TEXTURE_2D)
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_LINEAR)
GL.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_DECAL)
bgcolor = np.array(GL.glGetDoublev(GL.GL_COLOR_CLEAR_VALUE))
fgcolor = 1. - bgcolor
from .lines import Lines
sc = float(np.max(np.max(m.v, axis=0) - np.min(m.v, axis=0))) / 10.
cur_mtx = np.linalg.pinv(GL.glGetFloatv(GL.GL_MODELVIEW_MATRIX).T)
xdir = cur_mtx[:3, 0]
ydir = cur_mtx[:3, 1]
GL.glEnable(GL.GL_LINE_SMOOTH)
GL.glEnable(GL.GL_BLEND)
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
for vidx, text in m.v_to_text.items():
pos0 = m.v[vidx].copy()
pos1 = m.v[vidx].copy()
if hasattr(m, 'vn'):
pos1 += m.vn[vidx] * sc
GL.glLineWidth(5.0)
ln = Lines(v=np.vstack((pos0, pos1)), e=np.array([[0, 1]]))
GL.glEnable(GL.GL_LIGHTING)
GL.glColor3f(1. - 0.8, 1. - 0.8, 1. - 1.00)
MeshViewerSingle.draw_lines(ln)
GL.glDisable(GL.GL_LIGHTING)
texture_id = get_textureid_with_text(text, bgcolor, fgcolor)
GL.glBindTexture(GL.GL_TEXTURE_2D, texture_id)
GL.glPushMatrix()
GL.glTranslatef(pos1[0], pos1[1], pos1[2])
dx = xdir * .10
dy = ydir * .10
if False:
GL.glBegin(GL.GL_QUADS)
GL.glTexCoord2f(1., 0.)
GL.glVertex3f(*(+dx + dy))
GL.glTexCoord2f(1., 1.)
GL.glVertex3f(*(+dx - dy))
GL.glTexCoord2f(0., 1.)
GL.glVertex3f(*(-dx - dy))
GL.glTexCoord2f(0., 0.)
GL.glVertex3f(*(-dx + dy))
# gluSphere(quadratic,0.05,32,32)
GL.glEnd()
else:
GL.glBegin(GL.GL_POLYGON)
for r in np.arange(0, np.pi * 2., .01):
GL.glTexCoord2f(np.cos(r) / 2. + .5, np.sin(r) / 2. + .5)
GL.glVertex3f(*(dx * np.cos(r) + -dy * np.sin(r)))
GL.glEnd()
GL.glPopMatrix()
@staticmethod
def draw_lines(ls):
GL.glDisableClientState(GL.GL_NORMAL_ARRAY)
GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
GL.glLineWidth(3.0)
allpts = ls.v[ls.e.flatten()].astype(np.float32)
GL.glVertexPointerf(allpts)
if hasattr(ls, 'vc') or hasattr(ls, 'ec'):
GL.glEnableClientState(GL.GL_COLOR_ARRAY)
if hasattr(ls, 'vc'):
GL.glColorPointerf(ls.vc[ls.e.flatten()].astype(np.float32))
else:
clrs = np.ones((ls.e.shape[0] * 2, 3)) * np.repeat(ls.ec, 2, axis=0)
GL.glColorPointerf(clrs)
else:
GL.glDisableClientState(GL.GL_COLOR_ARRAY)
GL.glDisable(GL.GL_LIGHTING)
GL.glDrawElementsui(GL.GL_LINES, np.arange(len(allpts), dtype=np.uint32))
def draw_primitives(self,
scalefactor=1.0,
center=[0.0, 0.0, 0.0],
recenter=False,
want_camera=False):
# measure the bounding box of all our primitives, so that we can
# recenter them in our field of view
if recenter:
all_meshes = self.static_meshes + self.dynamic_meshes
all_lines = self.static_lines + self.dynamic_lines
if (len(all_meshes) + len(all_lines)) == 0:
if want_camera:
return {'modelview_matrix': GL.glGetDoublev(GL.GL_MODELVIEW_MATRIX),
'projection_matrix': GL.glGetDoublev(GL.GL_PROJECTION_MATRIX),
'viewport': GL.glGetIntegerv(GL.GL_VIEWPORT)
}
else:
return None
for m in all_meshes:
m.v = m.v.reshape((-1, 3))
all_verts = np.concatenate(
[m.v[m.f.flatten() if len(m.f) > 0 else np.arange(len(m.v))] for m in all_meshes] +
[l.v[l.e.flatten()] for l in all_lines],
axis=0)
maximum = np.max(all_verts, axis=0)
minimum = np.min(all_verts, axis=0)
center = (maximum + minimum) / 2.
scalefactor = (maximum - minimum) / 4.
scalefactor = np.max(scalefactor)
else:
center = np.array(center)
# for mesh in self.dynamic_meshes :
# if mesh.f : mesh.reset_normals()
all_meshes = self.static_meshes + self.dynamic_meshes
all_lines = self.static_lines + self.dynamic_lines
self.current_center = center
self.current_scalefactor = scalefactor
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glPushMatrix()
# uncomment to add a default rotation (useful when automatically snapshoting kinect data
# glRotate(220, 0.0, 1.0, 0.0)
tf = np.identity(4, 'f') / scalefactor
tf[:3, 3] = -center / scalefactor
tf[3, 3] = 1
cur_mtx = GL.glGetFloatv(GL.GL_MODELVIEW_MATRIX).T
GL.glLoadMatrixf(cur_mtx.dot(tf).T)
if want_camera:
result = {'modelview_matrix': GL.glGetDoublev(GL.GL_MODELVIEW_MATRIX),
'projection_matrix': GL.glGetDoublev(GL.GL_PROJECTION_MATRIX),
'viewport': GL.glGetIntegerv(GL.GL_VIEWPORT)
}
else:
result = None
for m in all_meshes:
if not hasattr(m, 'vbo'):
# Precompute vertex vbo
fidxs = m.f.flatten() if len(m.f) > 0 else np.arange(len(m.v))
allpts = m.v[fidxs].astype(np.float32).flatten()
vbo = VBO(allpts)
m.vbo = {'v': vbo}
# Precompute normals vbo
if hasattr(m, 'vn'):
ns = m.vn.astype(np.float32)
ns = ns[m.f.flatten(), :]
m.vbo['vn'] = VBO(ns.flatten())
elif hasattr(m, 'f') and m.f.size > 0:
ns = TriNormals(m.v, m.f).reshape(-1, 3)
ns = np.tile(ns, (1, 3)).reshape(-1, 3).astype(np.float32)
m.vbo['vn'] = VBO(ns.flatten())
# Precompute texture vbo
if hasattr(m, 'ft') and (m.ft.size > 0):
ftidxs = m.ft.flatten()
data = m.vt[ftidxs].astype(np.float32)[:, 0:2]
data[:, 1] = 1.0 - 1.0 * data[:, 1]
m.vbo['vt'] = VBO(data)
# Precompute color vbo
if hasattr(m, 'vc'):
data = m.vc[fidxs].astype(np.float32)
m.vbo['vc'] = VBO(data)
elif hasattr(m, 'fc'):
data = np.tile(m.fc, (1, 3)).reshape(-1, 3).astype(np.float32)
m.vbo['vc'] = VBO(data)
for e in all_lines:
self.draw_lines(e)
for m in all_meshes:
if hasattr(m, 'texture_image') and not hasattr(m, 'textureID'):
self.set_texture(m)
self.draw_mesh(m, self.lighting_on)
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glPopMatrix()
return result
class MeshViewerLocal:
"""Proxy viewer instance for visual inspection of geometric primitives.
The class forks another python process holding the display. It communicates
the commands with the remote instance seamlessly.
Write-only attributes:
:param titlebar: string printed in the window titlebar
:param dynamic_meshes: list of Mesh objects to be displayed
:param static_meshes: list of Mesh objects to be displayed
:param dynamic_lines: list of Lines objects to be displayed
:param static_lines: list of Lines objects to be displayed
.. note::
`static_meshes` is meant for Meshes that are
updated infrequently, and dynamic_meshes is for Meshes
that are updated frequently (same for dynamic_lines vs
static_lines). They may be treated differently for
performance reasons.
"""
managed = {}
def __new__(
cls,
titlebar=MESH_VIEWER_DEFAULT_TITLE,
uid=None,
host=ZMQ_HOST,
port=None,
shape=MESH_VIEWER_DEFAULT_SHAPE,
keepalive=False,
window_width=MESH_VIEWER_DEFAULT_WIDTH,
window_height=MESH_VIEWER_DEFAULT_HEIGHT
):
assert(uid is None or isinstance(uid, str))
if uid == 'stack':
uid = ''.join(traceback.format_list(traceback.extract_stack()))
if uid and uid in MeshViewer.managed.keys():
return MeshViewer.managed[uid]
viewer = super(MeshViewerLocal, cls).__new__(cls)
viewer.remote_host = host
viewer.remote_port = port
viewer.client = zmq.Context.instance().socket(zmq.PUSH)
viewer.client.linger = 0
if viewer.remote_port:
addr = "{}://{}:{}".format(
ZMQ_TRANSPORT,
viewer.remote_host,
viewer.remote_port)
viewer.client.connect(addr)
# XXX: Proper shape querying over the network is not
# possible as of now. This should be tackled during
# refactoring in the future.
# viewer.shape = viewer.get_window_shape()
viewer.shape = (1, 1)
return viewer
with open(os.devnull) as dev_null, \
tempfile.TemporaryFile() as err:
viewer.p = _run_self([titlebar, str(shape[0]), str(shape[1]), str(window_width), str(window_height)],
stdin=dev_null,
stdout=subprocess.PIPE,
stderr=err)
line = viewer.p.stdout.readline().decode()
viewer.p.stdout.close()
current_port = re.match('(.*?)', line)
if not current_port:
raise Exception("MeshViewer remote appears to have failed to launch")
current_port = int(current_port.group(1))
viewer.client.connect('{}://{}:{}'.format(ZMQ_TRANSPORT, ZMQ_HOST, current_port))
logging.info(
"started remote viewer on port {}".format(current_port))
if uid:
MeshViewerLocal.managed[uid] = viewer
viewer.shape = shape
viewer.keepalive = keepalive
return viewer
def get_subwindows(self):
return [[MeshSubwindow(parent_window=self, which_window=(r, c)) for c in range(self.shape[1])] for r in range(self.shape[0])]
@staticmethod
def _sanitize_meshes(list_of_meshes):
lm = []
# have to copy the meshes for now, because some contain CPython members,
# before pushing them on the queue
for m in list_of_meshes:
if hasattr(m, 'fc'):
lm.append(Mesh(v=m.v, f=m.f, fc=m.fc))
elif hasattr(m, 'vc'):
lm.append(Mesh(v=m.v, f=m.f, vc=m.vc))
else:
lm.append(Mesh(v=m.v, f=m.f if hasattr(m, 'f') else []))
if hasattr(m, 'vn'):
lm[-1].vn = m.vn
if hasattr(m, 'fn'):
lm[-1].fn = m.fn
if hasattr(m, 'v_to_text'):
lm[-1].v_to_text = m.v_to_text
if hasattr(m, 'texture_filepath') and hasattr(m, 'vt') and hasattr(m, 'ft'):
lm[-1].texture_filepath = m.texture_filepath
lm[-1].vt = m.vt
lm[-1].ft = m.ft
return lm
def _send_pyobj(self, label, obj, blocking, which_window):
logging.debug("sending a request:")
logging.debug("\tlabel = {!r}".format(label))
logging.debug("\tobj = {!r}".format(obj))
logging.debug("\tblocking = {!r}".format(blocking))
logging.debug("\twhich_window = {!r}".format(which_window))
if blocking:
context = zmq.Context.instance()
server = context.socket(zmq.PULL)
server.linger = 0
port = server.bind_to_random_port(
"{}://{}".format(ZMQ_TRANSPORT, ZMQ_HOST),
min_port=ZMQ_PORT_MIN,
max_port=ZMQ_PORT_MAX,
max_tries=100000)
# sending with blocking'
self.client.send_pyobj({
'label': label,
'obj': obj,
'port': port,
'which_window': which_window
})
task_completion_time = server.recv_pyobj()
# task completion time was %.2fs in other process' % (task_completion_time,)
server.close()
else:
# sending nonblocking
res = self.client.send_pyobj({
'label': label,
'obj': obj,
'which_window': which_window
})
def _recv_pyobj(self, label, port=None):
context = zmq.Context.instance()
server = context.socket(zmq.PULL)
server.linger = 0
if not port:
port = server.bind_to_random_port(
"{}://{}".format(ZMQ_TRANSPORT, ZMQ_HOST),
min_port=ZMQ_PORT_MIN,
max_port=ZMQ_PORT_MAX,
max_tries=100000)
self._send_pyobj(label, port, blocking=True, which_window=(0, 0))
result = server.recv_pyobj()
server.close()
return result
def set_dynamic_meshes(self, list_of_meshes, blocking=False, which_window=(0, 0)):
self._send_pyobj('dynamic_meshes', self._sanitize_meshes(list_of_meshes), blocking, which_window)
def set_static_meshes(self, list_of_meshes, blocking=False, which_window=(0, 0)):
self._send_pyobj('static_meshes', self._sanitize_meshes(list_of_meshes), blocking, which_window)
# list_of_model_names_and_parameters should be of form [{'name': scape_model_name, 'parameters': scape_model_parameters}]
# here scape_model_name is the filepath of the scape model.
def set_dynamic_models(self, list_of_model_names_and_parameters, blocking=False, which_window=(0, 0)):
self._send_pyobj('dynamic_models', list_of_model_names_and_parameters, blocking, which_window)
def set_dynamic_lines(self, list_of_lines, blocking=False, which_window=(0, 0)):
self._send_pyobj('dynamic_lines', list_of_lines, blocking, which_window)
def set_static_lines(self, list_of_lines, blocking=False, which_window=(0, 0)):
self._send_pyobj('static_lines', list_of_lines, blocking, which_window)
def set_titlebar(self, titlebar, blocking=False, which_window=(0, 0)):
self._send_pyobj('titlebar', titlebar, blocking, which_window)
def set_lighting_on(self, lighting_on, blocking=False, which_window=(0, 0)):
self._send_pyobj('lighting_on', lighting_on, blocking, which_window)
def set_autorecenter(self, autorecenter, blocking=False, which_window=(0, 0)):
self._send_pyobj('autorecenter', autorecenter, blocking, which_window)
def set_background_color(self, background_color, blocking=False, which_window=(0, 0)):
assert(isinstance(background_color, np.ndarray))
assert(background_color.size == 3)
self._send_pyobj('background_color', background_color.flatten(), blocking, which_window)
def get_keypress(self):
return self._recv_pyobj('get_keypress')
def get_mouseclick(self):
"""Returns a mouse click event.
.. note::
the call is blocking the caller until an event is received
"""
return self._recv_pyobj('get_mouseclick')
def get_event(self):
return self._recv_pyobj('get_event')
def get_window_shape(self):
response = self._recv_pyobj("get_window_shape")
return response["shape"]
background_color = property(fset=set_background_color,
doc="Background color, as 3-element numpy array where 0 <= color <= 1.0.")
dynamic_meshes = property(fset=set_dynamic_meshes,
doc="List of meshes for dynamic display.")
static_meshes = property(fset=set_static_meshes,
doc="List of meshes for static display.")
dynamic_models = property(fset=set_dynamic_models,
doc="List of model names and parameters for dynamic display.")
dynamic_lines = property(fset=set_dynamic_lines,
doc="List of Lines for dynamic display.")
static_lines = property(fset=set_static_lines,
doc="List of Lines for static display.")
titlebar = property(fset=set_titlebar,
doc="Titlebar string.")
def save_snapshot(self, path, blocking=False, which_window=(0, 0), wait_time=1):
"""Saves a snapshot of the current window into the specified file
:param path: filename to which the current window content will be saved
:param wait_time: waiting time to save snapshot. Increase it if the image is incomplete
"""
print('Saving snapshot to %s, please wait...' % path)
self._send_pyobj('save_snapshot', path, blocking, which_window)
time.sleep(wait_time)
def __del__(self):
if hasattr(self, "p") and not self.keepalive:
self.p.terminate()
class MeshViewerRemote:
def __init__(
self,
titlebar=MESH_VIEWER_DEFAULT_TITLE,
subwins_vert=MESH_VIEWER_DEFAULT_SHAPE[1],
subwins_horz=MESH_VIEWER_DEFAULT_SHAPE[0],
width=MESH_VIEWER_DEFAULT_WIDTH,
height=MESH_VIEWER_DEFAULT_HEIGHT,
port=None
):
context = zmq.Context.instance()
self.server = context.socket(zmq.PULL)
self.server.linger = 0
if not port:
self.port = self.server.bind_to_random_port(
"{}://{}".format(ZMQ_TRANSPORT, ZMQ_HOST),
min_port=ZMQ_PORT_MIN,
max_port=ZMQ_PORT_MAX,
max_tries=100000)
else:
self.server.bind(
"{}://{}:{}".format(ZMQ_TRANSPORT, ZMQ_HOST, port))
self.port = port
logging.debug(
"listening for incoming messages on port {}"
.format(self.port))
# Print out our port so that our client can connect to us with it. Flush stdout immediately; otherwise
# our client could wait forever.
print('%d\n' % (self.port,))
sys.stdout.flush()
self.arcball = ArcBallT(width, height)
self.transform = Matrix4fT()
self.lastrot = Matrix3fT()
self.thisrot = Matrix3fT()
self.isdragging = False
self.need_redraw = True
self.shape = (subwins_vert, subwins_horz)
self.mesh_viewers = [
[
MeshViewerSingle(
float(c) / (subwins_horz),
float(r) / (subwins_vert),
1. / subwins_horz,
1. / subwins_vert)
for c in range(subwins_horz)
]
for r in range(subwins_vert)
]
self.tm_for_fps = 0.
self.titlebar = titlebar
self.activate(width, height)
def snapshot(self, path, wait_time=0.1):
"""
Takes a snapshot of the meshviewer window and saves it to disc.
:param path: path to save the snapshot at.
:param wait_time: waiting time to save snapshot. Increase it if the image is incomplete
.. note:: Requires the Pillow package to be installed.
"""
self.on_draw()
time.sleep(wait_time)
width = GLUT.glutGet(GLUT.GLUT_WINDOW_WIDTH)
height = GLUT.glutGet(GLUT.GLUT_WINDOW_HEIGHT)
data = (GLU.GLubyte * (3 * width * height))(0)
GL.glReadPixels(0, 0, width, height, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, data)
image = Image.frombytes(mode="RGB", size=(width, height), data=data)
image = image.transpose(Image.FLIP_TOP_BOTTOM)
# Save image to disk
image.save(path)
def activate(self, width, height):
GLUT.glutInit(['mesh_viewer'])
GLUT.glutInitDisplayMode(GLUT.GLUT_RGBA | GLUT.GLUT_DOUBLE | GLUT.GLUT_ALPHA | GLUT.GLUT_DEPTH)
GLUT.glutInitWindowSize(width, height)
GLUT.glutInitWindowPosition(0, 0)
self.root_window_id = GLUT.glutCreateWindow(self.titlebar)
GLUT.glutTimerFunc(100, self.checkQueue, 0)
GLUT.glutReshapeFunc(self.on_resize_window)
GLUT.glutKeyboardFunc(self.on_keypress)
GLUT.glutMouseFunc(self.on_click)
GLUT.glutMotionFunc(self.on_drag)
GLUT.glutDisplayFunc(self.on_draw)
self.init_opengl()
GLUT.glutMainLoop() # won't return until process is killed
def on_drag(self, cursor_x, cursor_y):
""" Mouse cursor is moving
Glut calls this function (when mouse button is down)
and pases the mouse cursor postion in window coords as the mouse moves.
"""
from .geometry.rodrigues import rodrigues
if (self.isdragging):
mouse_pt = Point2fT(cursor_x, cursor_y)
ThisQuat = self.arcball.drag(mouse_pt) # // Update End Vector And Get Rotation As Quaternion
self.thisrot = Matrix3fSetRotationFromQuat4f(ThisQuat) # // Convert Quaternion Into Matrix3fT
# Use correct Linear Algebra matrix multiplication C = A * B
self.thisrot = Matrix3fMulMatrix3f(self.lastrot, self.thisrot) # // Accumulate Last Rotation Into This One
# make sure it is a rotation
self.thisrot = rodrigues(rodrigues(self.thisrot)[0])[0]
self.transform = Matrix4fSetRotationFromMatrix3f(self.transform, self.thisrot) # // Set Our Final Transform's Rotation From This One
GLUT.glutPostRedisplay()
return
# The function called whenever a key is pressed. Note the use of Python tuples to pass in: (key, x, y)
def on_keypress(self, *args):
key = args[0]
if hasattr(self, 'event_port'):
self.keypress_port = self.event_port
del self.event_port
if hasattr(self, 'keypress_port'):
client = zmq.Context.instance().socket(zmq.PUSH)
client.connect('{}://{}:{}'.format(ZMQ_TRANSPORT, ZMQ_HOST, self.keypress_port))
client.send_pyobj({'event_type': 'keyboard', 'key': key})
del self.keypress_port
def on_click(self, button, button_state, cursor_x, cursor_y):
""" Mouse button clicked.
Glut calls this function when a mouse button is
clicked or released.
"""
self.isdragging = False
if (button == GLUT.GLUT_LEFT_BUTTON and button_state == GLUT.GLUT_UP):
# Left button released
self.lastrot = copy.copy(self.thisrot) # Set Last Static Rotation To Last Dynamic One
elif (button == GLUT.GLUT_LEFT_BUTTON and button_state == GLUT.GLUT_DOWN):
# Left button clicked down
self.lastrot = copy.copy(self.thisrot) # Set Last Static Rotation To Last Dynamic One
self.isdragging = True # // Prepare For Dragging
mouse_pt = Point2fT(cursor_x, cursor_y)
self.arcball.click(mouse_pt) # Update Start Vector And Prepare For Dragging
elif (button == GLUT.GLUT_RIGHT_BUTTON and button_state == GLUT.GLUT_DOWN):
# If a mouse click location was requested, return it to caller
if hasattr(self, 'event_port'):
self.mouseclick_port = self.event_port
del self.event_port
if hasattr(self, 'mouseclick_port'):
self.send_mouseclick_to_caller(cursor_x, cursor_y)
elif (button == GLUT.GLUT_MIDDLE_BUTTON and button_state == GLUT.GLUT_DOWN):
# If a mouse click location was requested, return it to caller
if hasattr(self, 'event_port'):
self.mouseclick_port = self.event_port
del self.event_port
if hasattr(self, 'mouseclick_port'):
self.send_mouseclick_to_caller(cursor_x, cursor_y, button='middle')
GLUT.glutPostRedisplay()
def send_mouseclick_to_caller(self, cursor_x, cursor_y, button='right'):
client = zmq.Context.instance().socket(zmq.PUSH)
client.connect('{}://{}:{}'.format(ZMQ_TRANSPORT, ZMQ_HOST, self.mouseclick_port))
cameras = self.on_draw(want_cameras=True)
window_height = GLUT.glutGet(GLUT.GLUT_WINDOW_HEIGHT)
depth_value = GL.glReadPixels(cursor_x, window_height - cursor_y, 1, 1, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT)
pyobj = {
'event_type': 'mouse_click_%sbutton' % button,
'u': None, 'v': None,
'x': None, 'y': None, 'z': None,
'subwindow_row': None,
'subwindow_col': None
}
for subwin_row, camera_list in enumerate(cameras):
for subwin_col, camera in enumerate(camera_list):
# test for out-of-bounds
if cursor_x < camera['viewport'][0]:
continue
if cursor_x > (camera['viewport'][0] + camera['viewport'][2]):
continue
if window_height - cursor_y < camera['viewport'][1]:
continue
if window_height - cursor_y > (camera['viewport'][1] + camera['viewport'][3]):
continue
xx, yy, zz = GLU.gluUnProject(
cursor_x, window_height - cursor_y, depth_value,
camera['modelview_matrix'],
camera['projection_matrix'],
camera['viewport'])
pyobj = {
'event_type': 'mouse_click_%sbutton' % button,
'u': cursor_x - camera['viewport'][0], 'v': window_height - cursor_y - camera['viewport'][1],
'x': xx, 'y': yy, 'z': zz,
'which_subwindow': (subwin_row, subwin_col)
}
client.send_pyobj(pyobj)
del self.mouseclick_port
def on_draw(self, want_cameras=False):
# sys.stderr.write('fps: %.2e\n' % (1. / (time.time() - self.tm_for_fps)))
self.tm_for_fps = time.time()
GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
cameras = []
for mvl in self.mesh_viewers:
cameras.append([])
for mv in mvl:
cameras[-1].append(mv.on_draw(self.transform, want_cameras))
GL.glFlush() # Flush The GL Rendering Pipeline
GLUT.glutSwapBuffers()
self.need_redraw = False
if want_cameras:
return cameras
def on_resize_window(self, Width, Height):
"""Reshape The Window When It's Moved Or Resized"""
self.arcball.setBounds(Width, Height) # //*NEW* Update mouse bounds for arcball
return
def send_window_shape(self, port):
client = zmq.Context.instance().socket(zmq.PUSH)
client.connect('{}://{}:{}'.format(ZMQ_TRANSPORT, ZMQ_HOST, port))
client.send_pyobj({
'event_type': 'window_shape',
'shape': self.shape
})
def handle_request(self, request):
label = request['label']
obj = request['obj']
w = request['which_window']
mv = self.mesh_viewers[w[0]][w[1]]
logging.debug("received a request: {}".format(request))
# Handle each type of request.
# Some requests require a redraw, and
# some don't.
if label == 'dynamic_meshes':
mv.dynamic_meshes = obj
self.need_redraw = True
elif label == 'dynamic_models':
mv.dynamic_models = obj
self.need_redraw = True
elif label == 'static_meshes':
mv.static_meshes = obj
self.need_redraw = True
elif label == 'dynamic_lines':
mv.dynamic_lines = obj
self.need_redraw = True
elif label == 'static_lines':
mv.static_lines = obj
self.need_redraw = True
elif label == 'autorecenter':
mv.autorecenter = obj
self.need_redraw = True
elif label == 'titlebar':
assert(isinstance(obj, str))
self.titlebar = obj
GLUT.glutSetWindowTitle(obj)
elif label == 'lighting_on':
mv.lighting_on = obj
self.need_redraw = True
elif label == 'background_color':
GL.glClearColor(obj[0], obj[1], obj[2], 1.0)
self.need_redraw = True
elif label == 'save_snapshot': # redraws for itself
assert(isinstance(obj, str))
self.snapshot(obj)
elif label == 'get_keypress':
self.keypress_port = obj
elif label == 'get_mouseclick':
self.mouseclick_port = obj
elif label == 'get_event':
self.event_port = obj
elif label == 'get_window_shape':
self.send_window_shape(obj)
else:
return False # can't handle this request string
return True # handled the request string
def checkQueue(self, unused_timer_id):
GLUT.glutTimerFunc(20, self.checkQueue, 0)
try:
request = self.server.recv_pyobj(zmq.NOBLOCK)
except zmq.ZMQError as e:
if e.errno != zmq.EAGAIN:
raise # something wrong besides empty queue
return # empty queue, no problem
if not request:
return
while (request):
task_completion_time = time.time()
if not self.handle_request(request):
raise Exception('Unknown command string: %s' % (request['label']))
task_completion_time = time.time() - task_completion_time
if 'port' in request: # caller wants confirmation
port = request['port']
client = zmq.Context.instance().socket(zmq.PUSH)
client.connect('{}://{}:{}'.format(ZMQ_TRANSPORT, ZMQ_HOST, port))
client.send_pyobj(task_completion_time)
try:
request = self.server.recv_pyobj(zmq.NOBLOCK)
except zmq.ZMQError as e:
if e.errno != zmq.EAGAIN:
raise
request = None
if self.need_redraw:
GLUT.glutPostRedisplay()
def init_opengl(self):
"""A general OpenGL initialization function. Sets all of the initial parameters.
We call this right after our OpenGL window is created.
"""
GL.glClearColor(0.0, 0.0, 0.0, 1.0) # This Will Clear The Background Color To Black
GL.glClearDepth(1.0) # Enables Clearing Of The Depth Buffer
GL.glDepthFunc(GL.GL_LEQUAL) # The Type Of Depth Test To Do
GL.glEnable(GL.GL_DEPTH_TEST) # Enables Depth Testing
GL.glShadeModel(GL.GL_SMOOTH)
GL.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST) # Really Nice Perspective Calculations
GL.glEnable(GL.GL_LIGHT0)
GL.glEnable(GL.GL_LIGHTING)
GL.glEnable(GL.GL_COLOR_MATERIAL)
GL.glEnable(GL.GL_NORMALIZE) # important since we rescale the modelview matrix
return True
if __name__ == '__main__':
if len(sys.argv) == 2 and sys.argv[1] == 'TEST_FOR_OPENGL':
_test_for_opengl()
elif len(sys.argv) > 2:
m = MeshViewerRemote(titlebar=sys.argv[1],
subwins_vert=int(sys.argv[2]),
subwins_horz=int(sys.argv[3]),
width=int(sys.argv[4]),
height=int(sys.argv[5]))
else:
print("#" * 10)
print('Usage:')
print("python -m %s.%s arguments" % (__package__, os.path.splitext(os.path.basename(__file__))[0]))