Source code for abcview.widget.viewer_widget
#-******************************************************************************
#
# Copyright (c) 2013-2014,
# Sony Pictures Imageworks Inc. and
# Industrial Light & Magic, a division of Lucasfilm Entertainment Company Ltd.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Sony Pictures Imageworks, nor
# Industrial Light & Magic, nor the names of their contributors may be used
# to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#-******************************************************************************
import os
import sys
import math
import traceback
from functools import wraps
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4 import uic
from PyQt4 import QtOpenGL
import numpy
import numpy.linalg as linalg
import OpenGL
OpenGL.ERROR_CHECKING = True
from OpenGL.GL import *
from OpenGL.GLU import *
import imath
import alembic
import abcview
from abcview.io import Mode
from abcview.gl import GLCamera, GLICamera, GLScene
from abcview.gl import get_final_matrix
from abcview import log, style, config
# GL drawing mode map
GL_MODE_MAP = {
Mode.OFF: 0,
Mode.BOUNDS: 1,
Mode.FILL: GL_FILL,
Mode.LINE: GL_LINE,
Mode.POINT: GL_POINT
}
[docs]def update_camera(func):
"""
GL camera update decorator
"""
@wraps(func)
def with_wrapped_func(*args, **kwargs):
func(*args, **kwargs)
wid = args[0]
wid.camera.apply()
wid.updateGL()
wid.signal_camera_updated.emit(wid.camera)
wid.state.signal_state_change.emit()
return with_wrapped_func
[docs]def set_diffuse_light():
"""
Sets up the GL calls for the light that illuminates objects.
"""
ambient = (0.1, 0.1, 0.1, 1.0)
diffuse = (0.5, 1.0, 1.0, 1.0)
position = (90.0, 90.0, 150.0, 0.0)
front_mat_shininess = (60.0)
front_mat_specular = (0.0, 0.0, 0.0, 1.0)
front_mat_diffuse = (0.0, 0.0, 0.0, 1.0)
back_mat_shininess = (60.0)
back_mat_specular = (0.0, 0.0, 0.0, 1.0)
back_mat_diffuse = (0.0, 0.0, 0.0, 1.0)
lmodel_ambient = (0.0, 0.0, 0.0, 1.0)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glShadeModel(GL_SMOOTH)
glEnable(GL_DEPTH_TEST)
glDepthFunc(GL_LEQUAL)
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient)
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse)
#glLightfv(GL_LIGHT0, GL_POSITION, position)
glMaterialfv(GL_FRONT, GL_SHININESS, front_mat_shininess)
glMaterialfv(GL_FRONT, GL_SPECULAR, front_mat_specular)
glMaterialfv(GL_FRONT, GL_DIFFUSE, front_mat_diffuse)
glMaterialfv(GL_BACK, GL_SHININESS, back_mat_shininess)
glMaterialfv(GL_BACK, GL_SPECULAR, back_mat_specular)
glMaterialfv(GL_BACK, GL_DIFFUSE, back_mat_diffuse)
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient)
glLightModelfv(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE)
#glFrontFace(GL_CCW)
glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE)
[docs]def create_viewer_app(filepath=None):
"""
Creates a standalone viewer app. ::
>>> from abcview.widget.viewer_widget import create_viewer_app
>>> create_viewer_app("file.abc")
"""
from abcview.app import App
app = App(sys.argv)
# create the viewer widget
viewer = GLWidget()
viewer_group = QtGui.QGroupBox()
viewer_group.setLayout(QtGui.QVBoxLayout())
viewer_group.layout().setSpacing(0)
viewer_group.layout().setMargin(0)
viewer_group.layout().addWidget(viewer)
viewer_group.setWindowTitle("GLWidget")
# set default size
viewer_group.setMinimumSize(QtCore.QSize(100, 100))
viewer_group.resize(500, 300)
# display the viewer app
viewer_group.show()
viewer_group.raise_()
# override key press event handler
viewer_group.keyPressEvent = viewer.keyPressEvent
def load():
if filepath and os.path.exists(filepath):
viewer.add_file(filepath)
app.signal_starting_up.connect(load)
return app.exec_()
def message(info):
dialog = QtGui.QMessageBox()
dialog.setStyleSheet(style.DIALOG)
dialog.setText(info)
dialog.exec_()
class GLSplitter(QtGui.QSplitter):
def __init__(self, orientation, wipe=False):
super(GLSplitter, self).__init__(orientation)
self.wipe = wipe
[docs]class GLState(QtCore.QObject):
"""
Global GL viewer state manager. Manages list of Cameras and Scenes,
which can be shared between viewers.
"""
SECOND = 1000.0
signal_state_change = QtCore.pyqtSignal()
signal_play_fwd = QtCore.pyqtSignal()
signal_play_stop = QtCore.pyqtSignal()
signal_current_time = QtCore.pyqtSignal(float)
signal_current_frame = QtCore.pyqtSignal(int)
def __init__(self, fps=24.0):
"""
:param fps: play scene at frames per second (fps)
"""
super(GLState, self).__init__()
self.timer = QtCore.QTimer(self)
self.active_viewer = None
self.frames_per_second = fps
self.clear()
# for calculating frames per second during playback
self.fps = 0.0
self.__fps_timer = QtCore.QTimer(self)
self.__fps_timer.setInterval(self.SECOND)
self.connect(self.__fps_timer, QtCore.SIGNAL("timeout ()"),
self._fps_timer_cb)
def uid(self):
return id(self)
def __repr__(self):
return "<GLState %s>" % self.uid()
[docs] def clear(self):
"""
Clears the current state.
"""
log.debug("[%s.clear]" % self)
# stores all the GLScene objects
self.__scenes = []
# stores all the GLCamera objects
self.__cameras = {}
# min/max/current time information
self.__min = None
self.__max = None
self.__time = 0
# current frame
self.__frame = 0
# "is playing" bool toggle
self.__playing = False
# frames per second tracker
self.__fps_counter = 0
# update all the viewers
self.signal_state_change.emit()
def _get_cameras(self):
return self.__cameras.values()
def _set_cameras(self):
log.debug("use add_camera()")
cameras = property(_get_cameras, _set_cameras, doc="cameras")
def _get_scenes(self):
return self.__scenes
def _set_scenes(self):
log.debug("use add_scene() to add scenes")
scenes = property(_get_scenes, _set_scenes, doc="scenes")
[docs] def add_scene(self, scene):
"""
Adds a scene to the viewer session.
:param scene: GLScene object
"""
log.debug("[%s.add_scene] %s" % (self, scene))
if type(scene) == GLScene:
if scene not in self.__scenes:
scene.state = self
self.__scenes.append(scene)
else:
scene.visible = True
scene.set_time(self.current_time)
self.signal_state_change.emit()
[docs] def add_file(self, filepath):
"""
Generic add file method.
:param filepath: path to file to add
"""
log.debug("[%s.add_file] %s" % (self, filepath))
self.add_scene(GLScene(filepath))
[docs] def remove_scene(self, scene):
"""
Removes a given GLScene object from the master scene.
:param scene: GLScene to remove.
"""
log.debug("[%s.remove_scene] %s" % (self, scene))
scene.visible = False
if scene in self.__scenes:
self.__scenes.remove(scene)
self.signal_state_change.emit()
[docs] def add_camera(self, camera):
"""
Adds a new GLCamera to the state.
:param camera: GLCamera object
"""
log.debug("[%s.add_camera] %s" % (self, camera))
if camera and camera.name not in self.__cameras.keys():
self.__cameras[camera.name] = camera
return True
return False
[docs] def remove_camera(self, camera):
"""
Removes a GLCamera object from the state.
:param camera: GLCamera object
"""
if type(camera) in [str, unicode]:
del self.__cameras[name]
else:
del self.__cameras[camera.name]
self.signal_state_change.emit()
[docs] def get_camera(self, name):
"""
Returns a named camera for a given viewer.
:param camera: GLCamera object
"""
return self.__cameras.get(name)
def _get_time(self):
return self.__time
def _set_time(self, new_time):
if new_time is None:
log.warn("time is None")
return
self.__time = new_time
self.__frame = new_time * self.frames_per_second
for scene in self.scenes:
if scene.visible:
scene.set_time(new_time)
#log.debug("[%s._set_time] %s" % (self, self.__time))
self.signal_current_time.emit(new_time)
self.signal_current_frame.emit(int(round(new_time * self.frames_per_second)))
# update other viewers
self.signal_state_change.emit()
current_time = property(_get_time, _set_time, doc="set/get current time")
def _get_frame(self):
return self.__frame
def _set_frame(self, frame):
if frame > self.frame_range()[1]:
frame = self.frame_range()[0]
elif frame < self.frame_range()[0]:
frame = self.frame_range()[1]
self.current_time = frame / float(self.frames_per_second)
current_frame = property(_get_frame, _set_frame, doc="set/get current frame")
def _get_min_time(self):
return self.__min
def _set_min_time(self, value):
self.__min = value
log.debug("[%s._set_min_time] %s" % (self, self.__min))
min_time = property(_get_min_time, _set_min_time, doc="set/get minimum time")
def _get_max_time(self):
return self.__max
def _set_max_time(self, value):
self.__max = value
log.debug("[%s._set_min_time] %s" % (self, self.__max))
max_time = property(_get_max_time, _set_max_time, doc="set/get maximum time")
[docs] def time_range(self):
"""
Returns min/max time range in seconds as a tuple.
"""
#log.debug("[%s.time_range] %s, %s" % (self, self.__min, self.__max))
if self.__min == None or self.__max == None:
if self.scenes:
for scene in self.scenes:
if self.__min is None or scene.min_time() < min:
self.__min = scene.min_time()
if self.__max is None or scene.max_time() > max:
self.__max = scene.max_time()
else:
self.__min, self.__max = 0, 0
return (self.__min, self.__max)
def _get_min_frame(self):
return self.frame_range()[0]
def _set_min_frame(self, value):
if value is None:
self.min_time = None
elif self.frames_per_second > 0:
self.min_time = value / float(self.frames_per_second)
min_frame = property(_get_min_frame, _set_min_frame, doc="set/get minimum frame")
def _get_max_frame(self):
return self.frame_range()[1]
def _set_max_frame(self, value):
if value is None:
self.max_time = None
elif self.frames_per_second > 0:
self.max_time = value / float(self.frames_per_second)
max_frame = property(_get_max_frame, _set_max_frame, doc="set/get maximum frame")
[docs] def frame_range(self):
"""
Returns min/max frame range as a tuple.
"""
(min, max) = self.time_range()
return (min * self.frames_per_second, max * self.frames_per_second)
[docs] def frame_count(self):
"""
Returns total frame count.
"""
return len(range(*self.frame_range())) + 1
[docs] def is_playing(self):
"""
Returns True if the playback timer is running.
"""
return self.__playing
[docs] def play(self):
"""
Plays loaded scenes by activating timer and setting callback
"""
self.timer.setInterval(self.SECOND / (float(self.frames_per_second)))
self.connect(self.timer, QtCore.SIGNAL("timeout ()"), self._play_fwd_cb)
self.timer.start()
self.__fps_timer.start()
self.signal_play_fwd.emit()
def _play_fwd_cb(self):
"""
Play callback, sets current time for all scenes
"""
self.__playing = True
min_time, max_time = self.time_range()
self.current_time += 1.0 / float(self.frames_per_second)
if self.current_time > max_time:
self.current_time = min_time
self.__fps_counter += 1
[docs] def stop(self):
"""
Stops scene playback
"""
self.__playing = False
self.disconnect(self.timer, QtCore.SIGNAL("timeout ()"), self._play_fwd_cb)
self.timer.stop()
self.__fps_timer.stop()
self.__fps_counter = 0
self.fps = 0.0
self.signal_play_stop.emit()
def _fps_timer_cb(self):
"""
Frames per second timer callback
"""
self.fps = (self.__fps_counter / float(self.frames_per_second)) * \
self.frames_per_second
self.__fps_counter = 0
[docs]class GLWidget(QtOpenGL.QGLWidget):
"""
AbcView OpenGL Widget.
Basic usage ::
>>> create_viewer_app("file.abc")
or inside a larger Qt application ::
>>> viewer = GLWidget()
>>> viewer.add_file("file.abc")
"""
# scene signals
signal_scene_opened = QtCore.pyqtSignal(GLScene)
signal_scene_removed = QtCore.pyqtSignal(GLScene)
signal_scene_error = QtCore.pyqtSignal(str)
signal_scene_drawn = QtCore.pyqtSignal()
# camera signals (pass 'object' to support both camera classes)
signal_set_camera = QtCore.pyqtSignal(object)
signal_new_camera = QtCore.pyqtSignal(object)
signal_camera_updated = QtCore.pyqtSignal(object)
# selection signals
signal_scene_selected = QtCore.pyqtSignal(GLScene)
signal_object_selected = QtCore.pyqtSignal(str)
signal_clear_selection = QtCore.pyqtSignal()
# error signals
signal_undrawable_scene = QtCore.pyqtSignal(GLScene, float)
def __init__(self, parent=None, state=None):
"""
:param parent: parent Qt object
:param state: GLState object (for shared states)
"""
self.camera = None
format = QtOpenGL.QGLFormat()
format.setDirectRendering(True)
format.setSampleBuffers(True)
self.state = state or GLState()
self.state.signal_state_change.connect(self.handle_state_change)
super(GLWidget, self).__init__(format, parent)
self.setAutoBufferSwap(True)
self.setMouseTracking(True)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
# embedded in larger app?
self._main = parent
# various matrices and vectors
self.__bounds_default = imath.Box3d((-5,-5,-5), (5,5,5))
self.__bounds = None
self.__radius = 5.0
self.__last_pok = False
self.__last_p2d = QtCore.QPoint()
self.__last_p3d = [1.0, 0.0, 0.0]
self.__rotating = False
self.__mode = GL_SELECT
# viewers must have at least one camera
self.setup_default_camera()
def uid(self):
return id(self)
def __repr__(self):
return "<GLWidget %s>" % self.uid()
[docs] def setup_default_camera(self):
"""
Creates the default interactive camera for this view (and others if the
state is shared between viewers).
"""
self.add_camera(GLCamera(self, name="interactive"))
self.set_camera("interactive")
[docs] def clear(self):
"""
Resets this GL viewer widget, clears the shared state
and creates a new default interactive camera.
"""
self.state.clear()
self.setup_default_camera()
[docs] def add_file(self, filepath):
"""
Loads a given filepath into the GL viewer.
:param filepath: path to Alembic file.
"""
self.add_scene(GLScene(filepath))
[docs] def add_scene(self, scene):
"""
Adds a scene to the viewer session.
:param scene: GLScene object
"""
log.debug("[%s.add_scene] %s" % (self, scene))
self.state.add_scene(scene)
self.signal_scene_opened.emit(scene)
self.updateGL()
[docs] def remove_scene(self, scene):
"""
Removes a given GLScene object from the master scene.
:param scene: GLScene to remove.
"""
log.debug("[%s.remove_scene] %s" % (self, scene))
self.state.remove_scene(scene)
self.signal_scene_removed.emit(scene)
self.updateGL()
[docs] def add_camera(self, camera):
"""
:param camera: GLCamera object
"""
log.debug("[%s.add_camera] %s" % (self, camera))
if self.state.add_camera(camera):
camera.add_view(self)
self.signal_new_camera.emit(camera)
[docs] def remove_camera(self, camera):
"""
:param camera: GLCamera object to remove
"""
log.debug("[%s.remove_camera] %s" % (self, camera))
self.state.remove_camera(camera)
self.signal_state_change.emit()
@update_camera
[docs] def set_camera(self, camera):
"""
Sets the scene camera from a given camera name string
:param camera: Name of camera or GLCamera object
"""
log.debug("[%s.set_camera] %s" % (self, camera))
if type(camera) in [str, unicode]:
if "/" in camera:
camera = os.path.split("/")[-1]
elif camera not in [cam.name for cam in self.state.cameras]:
log.warn("camera not found: %s" % camera)
return
self.camera = self.state.get_camera(camera)
else:
self.camera = camera
self.camera.add_view(self)
self.resizeGL(self.width(), self.height())
self.signal_set_camera.emit(self.camera)
[docs] def aspect_ratio(self):
"""
Returns current aspect ration of the viewer.
"""
return self.width() / float(self.height())
def _get_bounds(self):
#TODO: ugly code needs refactor
if self.state.scenes:
bounds = None
for scene in self.state.scenes:
if not scene.loaded or not scene.drawable():
continue
if bounds is None or \
scene.bounds(self.state.current_time).max() > \
bounds.max():
bounds = scene.bounds(self.state.current_time)
min = bounds.min()
max = bounds.max()
if scene.properties.get("translate"):
max = max * imath.V3d(*scene.translate)
min = min * imath.V3d(*scene.translate)
if scene.properties.get("scale"):
max = max * imath.V3d(*scene.scale)
min = min * imath.V3d(*scene.scale)
bounds = imath.Box3d(min, max)
if bounds is None:
return self.__bounds_default
self.__bounds = bounds
return self.__bounds
else:
return self.__bounds_default
def _set_bounds(self, bounds):
self.__bounds = bounds
bounds = property(_get_bounds, _set_bounds, doc="scene bounding box")
@update_camera
[docs] def frame(self, bounds=None):
"""
Frames the viewer's active camera on the bounds of the currently
loaded and visible scenes.
:param bounds: imath.Box3d bounds object.
"""
log.debug("[%s.frame] %s" % (self, bounds))
if self.camera.type() == GLICamera.type():
message("""Can't frame when viewing through ICameras.\nSelect or create a new camera.""")
return
if bounds is None:
bounds = self.bounds
if self.camera:
self.camera.frame(bounds)
[docs] def split(self, orientation=QtCore.Qt.Vertical,
wipe=False):
"""
Splist the viewer into two separate widgets according
to the orientation param. ::
QGroupBox
`- QSplitter
|- QGroupBox
| `- GLWidget
`- QGroupBox
`- GLWidget
"""
if not self.parent():
return
item = self.parent().layout().itemAt(0)
# create the splitter
splitter = GLSplitter(orientation, wipe)
self.parent().layout().addWidget(splitter)
# left/top viewer group
group1 = QtGui.QGroupBox()
group1.setLayout(QtGui.QVBoxLayout())
group1.layout().setSpacing(0)
group1.layout().setMargin(0)
group1.layout().addWidget(item.widget())
# right/bottom viewer group
group2 = QtGui.QGroupBox()
group2.setLayout(QtGui.QVBoxLayout())
group2.layout().setSpacing(0)
group2.layout().setMargin(0)
new_viewer = GLWidget(self._main, state=self.state)
self.camera.add_view(new_viewer)
group2.layout().addWidget(new_viewer)
# link the two groups for unsplitting later
group1.other = group2
group1.viewer = self
group2.other = group1
group2.viewer = new_viewer
# add the two groups to the splitter
splitter.addWidget(group1)
splitter.addWidget(group2)
#TODO: support viewer pop-outs, better garbage collection
[docs] def unsplit(self):
"""
Unsplits and deletes current viewer
"""
if not self.parent():
return
# get the parent splitter object
splitter = self.parent()
while splitter and type(splitter) != GLSplitter:
splitter = splitter.parent()
# we've reached the top splitter, do nothing
if splitter is None:
return
splitter.parent().layout().removeWidget(splitter)
splitter.parent().layout().addWidget(self.parent().other)
# delete this view from all cameras
for camera in self.state.cameras:
camera.remove_view(self)
# reassign the viewer attribute on main
if self._main:
self._main.viewer = self.parent().other.viewer
# cleanup
del splitter
del self
def _paint_normals(self):
"""
Paints normals for polys and subds.
"""
glColor3f(1, 1, 1)
def _draw(obj):
md = obj.getMetaData()
if alembic.AbcGeom.IPolyMesh.matches(md) or alembic.AbcGeom.ISubD.matches(md):
meshObj = alembic.AbcGeom.IPolyMesh(obj.getParent(), obj.getName())
mesh = meshObj.getSchema()
ts = mesh.getTimeSampling()
index = ts.getNearIndex(self.state.current_time, mesh.getNumSamples())
facesProp = mesh.getFaceIndicesProperty()
pointsProp = mesh.getPositionsProperty()
normalsProp = mesh.getNormalsParam().getValueProperty()
for p in [facesProp, pointsProp, normalsProp]:
if not p.valid():
return
faces = facesProp.getValue(index)
points = pointsProp.getValue(index)
normals = normalsProp.getValue(index)
xf = get_final_matrix(meshObj, self.state.current_time)
for i, fi in enumerate(faces):
p = points[fi] * xf
n = normals[i]
v = p + n
glBegin(GL_LINES)
glColor3f(0, 1, 0)
glVertex3f(p[0], p[1], p[2])
glVertex3f(v[0], v[1], v[2])
glEnd()
for child in obj.children:
try:
_draw(child)
except Exception, e:
log.warn("unhandled exception: %s" % e)
for scene in self.state.scenes:
_draw(scene.top())
def _paint_grid(self):
"""
Paints the grid.
"""
glDisable(GL_LIGHTING)
glColor3f(0.5, 0.5, 0.5)
for x in range(-10, 11):
if x == 0:
continue
glBegin(GL_LINES)
glVertex3f(x, 0, -10)
glVertex3f(x, 0, 10)
glVertex3f(-10, 0, x)
glVertex3f(10, 0, x)
glEnd()
glBegin(GL_LINES)
glVertex3f(0, 0, -10)
glVertex3f(0, 0, 10)
glVertex3f(-10, 0, 0)
glVertex3f(10, 0, 0)
glEnd()
def _paint_hud(self):
"""
Paints the heads-up-display information.
"""
glColor3f(1, 1, 1)
def _format(array):
return ", ".join(["%.2f" %f for f in array])
glViewport(0, 0, self.width(), self.height())
# draw the camera name
glColor3f(0.5, 1, 0.5)
if self.camera.fixed:
self.renderText(15, 20, "%s [%.02f]"
% (self.camera.name, self.camera.aspect_ratio))
else:
self.renderText(15, 20, "%s" % self.camera.name)
# draw the camera info
glColor3f(0.6, 0.6, 0.6)
font = QtGui.QFont("Arial", 8)
self.renderText(15, 35, "T [%s]"
% _format(self.camera.translation), font)
self.renderText(15, 50, "R [%s]"
% _format(self.camera.rotation), font)
self.renderText(15, 66, "S [%s]"
% _format(self.camera.scale), font)
# draw the FPS info
glColor3f(0.7, 0.7, 0.7)
font = QtGui.QFont("Arial", 9)
self.renderText(self.width()-100, self.height()-10, "%.1f / %.1f FPS"
% (self.state.fps, self.state.frames_per_second), font)
glColor3f(1, 1, 1)
def _paint_fixed(self):
"""
Changes the GL viewport according to the camera's aspect ratio.
"""
# get basic size values for this viewer
camera_width = self.camera.get_size(self)[0]
camera_height = self.camera.get_size(self)[1]
camera_aspect_ratio = camera_width / float(camera_height)
# if fixed, lock the aspect ratio
if self.camera.fixed:
camera_aspect_ratio = self.camera.aspect_ratio
if self.aspect_ratio() > self.camera.aspect_ratio:
w = int(self.width() / (self.aspect_ratio() \
/ camera_aspect_ratio))
h = camera_height
else:
w = camera_width
h = int(self.height() * (self.aspect_ratio() \
/ camera_aspect_ratio))
x = int(abs(w-self.width()) / 2.0)
y = int(abs(h-self.height()) / 2.0)
# camera size matches viewer
else:
x = y = 0
w = self.width()
h = self.height()
# do some GL stuff
glViewport(x, y, w, h)
self.makeCurrent()
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(self.camera.fovy, camera_aspect_ratio,
self.camera.near, self.camera.far)
glMatrixMode(GL_MODELVIEW)
def set_camera_clipping_planes(self, near=None, far=None):
self.camera.set_clipping_planes(self.camera.near, self.camera.far)
def map_to_sphere(self, v2d):
v3d = [0.0, 0.0, 0.0]
if ((v2d.x() >= 0) and (v2d.x() <= self.width()) and
(v2d.y() >= 0) and (v2d.y() <= self.height())):
x = float(v2d.x() - 0.5 * self.width()) / self.width()
y = float(0.5 * self.height() - v2d.y()) / self.height()
v3d[0] = x
v3d[1] = y
z2 = 2.0 * 0.5 * 0.5 - x * x - y * y
v3d[2] = math.sqrt(max( z2, 0.0 ))
n = linalg.norm(v3d)
v3d = numpy.array(v3d) / float(n)
return True, v3d
else:
return False, v3d
[docs] def handle_set_camera(self, action):
"""
Sets camera from name derived from the text of a QAction.
:param action: QAction
"""
self.set_camera(str(action.text().toAscii()))
[docs] def handle_set_mode(self, mode):
"""
Set active camera drawing mode.
:param mode: abcview.io.Mode enum value
"""
self.setCursor(QtCore.Qt.WaitCursor)
if self.sender(): # via menu action
mode = self.sender().data().toInt()[0]
if mode not in GL_MODE_MAP.keys():
raise Exception("Invalid drawing mode: %s" % mode)
self.camera.mode = mode
self.setCursor(QtCore.Qt.ArrowCursor)
[docs] def handle_camera_action(self, action):
"""
New camera menu handler.
:param action: QAction object
"""
action_name = str(action.text().toAscii())
if action_name == "New":
text, ok = QtGui.QInputDialog.getText(self, "New Camera",
"Camera Name:", QtGui.QLineEdit.Normal)
if ok and not text.isEmpty():
name = str(text.toAscii())
camera = GLCamera(self, name)
self.add_camera(camera)
self.set_camera(name)
[docs] def selection(self, x, y):
"""
Bounding box selection handler. This handles selecting at the
scene level, e.g. GLScenes. Object-level selection is handled
in the AbcOpenGL lib.
:param x: mouse x position
:param y: mouse y position
:return: list of GLScene objects
"""
log.debug("[%s.selection] %s %s %s" % (self, x, y, self.camera))
self.setDisabled(True)
#--- begin adjustments for fixed aspect ratio cameras
#TODO: consolidate this code and same from _paint_fixed()
# get basic size values for this viewer
camera_width = self.camera.get_size(self)[0]
camera_height = self.camera.get_size(self)[1]
camera_aspect_ratio = camera_width / float(camera_height)
# if fixed, lock the aspect ratio
if self.camera.fixed:
camera_aspect_ratio = self.camera.aspect_ratio
if self.aspect_ratio() > self.camera.aspect_ratio:
w = int(self.width() / (self.aspect_ratio() \
/ camera_aspect_ratio))
h = camera_height
else:
w = camera_width
h = int(self.height() * (self.aspect_ratio() \
/ camera_aspect_ratio))
_x = int(abs(w-self.width()) / 2.0)
_y = int(abs(h-self.height()) / 2.0)
# camera size matches viewer
else:
_x = _y = 0
w = self.width()
h = self.height()
#--- end fixed ratio adjustments
MaxSize = 512
#viewport = glGetIntegerv(GL_VIEWPORT)
viewport = [_x, _y, w, h]
buffer = glSelectBuffer(MaxSize)
glRenderMode(GL_SELECT)
glInitNames()
# adjust mouse y value
if self.camera.fixed:
y = y - (_y * 2)
glMatrixMode(GL_PROJECTION)
glPushMatrix()
glLoadIdentity()
gluPickMatrix(x, (viewport[3] - y), 5.0, 5.0, viewport)
# adjust ratio for fixed cameras
if self.camera.fixed:
ratio = self.camera.aspect_ratio
else:
ratio = self.aspect_ratio()
gluPerspective(self.camera.fovy, camera_aspect_ratio,
self.camera.near, self.camera.far)
# draw the scenes
for scene in self.state.scenes:
# skip non-visible scenes
if not scene.visible:
continue
#TODO: pick on translated scenes
# push local transforms
#glPushMatrix()
#glTranslatef(*scene.translate)
#glRotatef(*scene.rotate)
#glScalef(*scene.scale)
# pick anywhere within the scene bounds
if scene.mode != Mode.OFF:
mode = GL_POLYGON
# pick just on the bounding box edges
else:
mode = GL_LINES
# draw scene bounds
scene.draw_bounds(self.state.current_time, mode)
# pop local transforms
#glPopMatrix()
#glMatrixMode(GL_PROJECTION)
glPopMatrix()
# get the list of gl picks
hits = glRenderMode(GL_RENDER)
self.setDisabled(False)
# return list of picked scenes
return self.state.scenes[hits[-1].names[-1]] if hits else None
def isReady(self):
if not self.isVisible():
return False
elif not self.isEnabled():
return False
elif not self.isValid():
return False
elif self not in self.camera.views:
return False
elif not self.state:
return False
elif self.isHidden():
return False
else:
return True
## base class overrides
def initializeGL(self):
self.makeCurrent()
glPointSize(1.0)
glEnable(GL_BLEND)
glEnable(GL_DEPTH_TEST)
glEnable(GL_AUTO_NORMAL)
glEnable(GL_COLOR_MATERIAL)
glEnable(GL_NORMALIZE)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glDisable(GL_CULL_FACE)
glShadeModel(GL_SMOOTH)
glClearColor(0.15, 0.15, 0.15, 0.0)
set_diffuse_light()
[docs] def paintGL(self):
"""
OpenGL painting override
"""
if not self.isReady():
return
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glDrawBuffer(GL_BACK)
glLoadIdentity()
# update camera
self.camera.apply()
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
# light0 tracks with camera
glLightfv(GL_LIGHT0, GL_POSITION, ( 0.0, 0.0, 1.0, 1.0 ))
glPopMatrix()
# adjusts the camera size
self._paint_fixed()
# draw the grid lines
if self.camera.draw_grid:
self._paint_grid()
# draw geom normals
if self.camera.draw_normals:
self._paint_normals()
# draw each scene
for scene in self.state.scenes:
if not scene.visible:
continue
if not scene.drawable():
#self.signal_undrawable_scene.emit(scene, self.state.current_frame)
glColor3d(1, 1, 0)
self.renderText(0, 0, 0, "[Error drawing %s]" % scene.name)
continue
# color override
glEnable(GL_COLOR_MATERIAL)
glMaterialfv(GL_BACK, GL_EMISSION, scene.color)
# draw mode override
mode = GL_MODE_MAP.get(scene.properties.get("mode"),
GL_MODE_MAP.get(self.camera.mode, GL_LINE)
)
if mode > 1:
glPolygonMode(GL_FRONT_AND_BACK, mode)
# apply local transforms
glPushMatrix()
glTranslatef(*scene.translate)
glRotatef(*scene.rotate)
glScalef(*scene.scale)
if scene.selected:
glColor3d(1, 1, 0)
else:
glColor3f(*scene.color)
# draw scene bounds
if self.camera.draw_bounds:
scene.draw_bounds(self.state.current_time)
set_diffuse_light()
# draw scene geom
if mode != Mode.OFF:
scene.draw(self.camera.visible, mode == Mode.BOUNDS)
# draw scene labels
if self.camera.draw_labels:
c = scene.bounds().center()
self.renderText(c[0], c[1], c[2], scene.name)
glPopMatrix()
self.signal_scene_drawn.emit()
# draw the heads-up-display
if self.camera.draw_hud:
self._paint_hud()
def resizeGL(self, width, height):
try:
self.camera.resize()
except AttributeError, e:
pass
@update_camera
[docs] def keyPressEvent(self, event):
"""
key press event handler
"""
key = event.key()
mod = event.modifiers()
def _set_selected_scene_mode(mode):
# applies display mode to objects/scenes
self.setCursor(QtCore.Qt.WaitCursor)
found = False
for scene in self.state.scenes:
if scene.selected:
scene.mode = mode
found = True
if mode != Mode.OFF:
scene.load()
if not found:
self.handle_set_mode(mode)
self.state.current_frame = self.state.current_frame
self.updateGL()
self.setCursor(QtCore.Qt.ArrowCursor)
# space bar - playback control
if key == QtCore.Qt.Key_Space:
if len(self.state.scenes) == 0 or \
self.state.frame_count <= 1:
return
if self.state.is_playing():
self.state.stop()
else:
self.state.play()
# 0 - display off
elif key == QtCore.Qt.Key_0:
_set_selected_scene_mode(Mode.OFF)
# 1 - smooth
elif key == QtCore.Qt.Key_1:
_set_selected_scene_mode(Mode.FILL)
# 2 - lines
elif key == QtCore.Qt.Key_2:
_set_selected_scene_mode(Mode.LINE)
# 3 - points
elif key == QtCore.Qt.Key_3:
_set_selected_scene_mode(Mode.POINT)
# 4 - bounds
elif key == QtCore.Qt.Key_4:
_set_selected_scene_mode(Mode.BOUNDS)
# right arroy - increment frame
elif key == QtCore.Qt.Key_Right:
self.state.current_frame = self.state.current_frame + 1
# left arrow - decrement frame
elif key == QtCore.Qt.Key_Left:
self.state.current_frame = self.state.current_frame - 1
# shift+f - toggle fixed aspect ratio
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_A:
self.camera._set_fixed()
# shift+b - toggle bounding boxes
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_B:
self.camera._set_draw_bounds()
# shift+n - toggle draw normals
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_N:
self.camera._set_draw_normals()
# shift+g - toggle grid
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_G:
self.camera._set_draw_grid()
# shift+h - toggle hud
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_H:
self.camera._set_draw_hud()
# shift+l - toggle labels
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_L:
self.camera._set_draw_labels()
# shift+v - toggle visibility
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_V:
self.camera._set_visible()
# shift+| - split vertically
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_Bar:
self.split_vert()
# shift+| - split horizontally
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_Underscore:
self.split_horz()
# shift+- - unsplit
elif mod == QtCore.Qt.ShiftModifier and key == QtCore.Qt.Key_Minus:
self.unsplit()
# "f" - frame scene
elif key == QtCore.Qt.Key_F:
self.frame()
[docs] def mouseDoubleClickEvent(self, event):
"""
mouse double-click event handler
"""
if self.camera.mode == Mode.OFF:
return
# get scene selection hits
hit = None
for scene in self.state.scenes:
if scene.mode == Mode.OFF or not scene.visible:
continue
x, y = event.pos().x(), event.pos().y()
camera = self.camera.views[self]
hit = scene.selection(x, y, camera)
#HACK: need a better/faster way to find the object
if hit:
name = hit.split("/")[-1]
self.signal_object_selected.emit(".*%s" % name)
[docs] def mousePressEvent(self, event):
"""
mouse press event handler
"""
#TODO: use weakref for this instead?
self.state.active_viewer = self
# make this viewer the active one
if self._main:
self._main.viewer = self
# process mouse event
if event.button() == QtCore.Qt.LeftButton and \
event.modifiers() == QtCore.Qt.NoModifier:
self.setCursor(QtCore.Qt.ArrowCursor)
if self.__mode == GL_SELECT:
for scene in self.state.scenes:
scene.selected = False
self.signal_clear_selection.emit()
hit = self.selection(event.pos().x(), event.pos().y())
if hit:
self.signal_scene_selected.emit(hit)
hit.selected = True
self.paintGL()
return
# process mouse event
elif event.button() == QtCore.Qt.RightButton and \
event.modifiers() == QtCore.Qt.NoModifier:
self.setCursor(QtCore.Qt.ArrowCursor)
menu = QtGui.QMenu(self)
# splits
layout_menu = QtGui.QMenu("Layout", self)
self.splitHAct = QtGui.QAction(QtGui.QIcon("%s/split_vert.png" % config.ICON_DIR),
"Split Vertical ", self)
self.splitHAct.setShortcut("Shift+|")
self.connect(self.splitHAct, QtCore.SIGNAL("triggered (bool)"), self.split_vert)
layout_menu.addAction(self.splitHAct)
self.splitVAct = QtGui.QAction(QtGui.QIcon("%s/split_horz.png" % config.ICON_DIR),
"Split Horizontal ", self)
self.splitVAct.setShortcut("Shift+_")
self.connect(self.splitVAct, QtCore.SIGNAL("triggered (bool)"), self.split_horz)
layout_menu.addAction(self.splitVAct)
self.closeAct = QtGui.QAction("Close ", self)
self.closeAct.setShortcut("Shift+-")
self.connect(self.closeAct, QtCore.SIGNAL("triggered (bool)"), self.unsplit)
layout_menu.addAction(self.closeAct)
menu.addMenu(layout_menu)
# cameras
camera_menu = QtGui.QMenu("Cameras", self)
camera_group = QtGui.QActionGroup(camera_menu)
camera_group_unsaved = QtGui.QActionGroup(camera_menu)
camera_group.setExclusive(False)
camera_group_unsaved.setExclusive(True)
for camera in self.state.cameras:
camera_action = QtGui.QAction(camera.name, self)
camera_action.setCheckable(True)
if camera.name == self.camera.name:
camera_action.setChecked(True)
if camera.name == "interactive":
camera_action.setActionGroup(camera_group_unsaved)
camera_action.setToolTip("This camera cannot be saved.")
else:
camera_action.setActionGroup(camera_group)
camera_menu.addAction(camera_action)
self.connect(camera_group, QtCore.SIGNAL("triggered (QAction *)"),
self.handle_set_camera)
self.connect(camera_group_unsaved, QtCore.SIGNAL("triggered (QAction *)"),
self.handle_set_camera)
self.connect(camera_menu, QtCore.SIGNAL("triggered (QAction *)"),
self.handle_camera_action)
camera_menu.addSeparator()
camera_menu.addAction("New")
menu.addMenu(camera_menu)
options_menu = QtGui.QMenu("Options", self)
# bounds toggle menu item
#self.aframeAct = QtGui.QAction("Autoframe", self)
#self.aframeAct.setCheckable(True)
#self.aframeAct.setChecked(self.camera.auto_frame)
#self.connect(self.aframeAct, QtCore.SIGNAL("toggled (bool)"),
# self.camera._set_auto_frame)
#options_menu.addAction(self.aframeAct)
# fixed aspect ratio toggle menu item
self.fixedAct = QtGui.QAction("Fixed Aspect Ratio ", self)
self.fixedAct.setShortcut("Shift+A")
self.fixedAct.setCheckable(True)
self.fixedAct.setChecked(self.camera.fixed)
self.connect(self.fixedAct, QtCore.SIGNAL("toggled (bool)"),
self.camera._set_fixed)
options_menu.addAction(self.fixedAct)
# heads-up-display menu item
self.hudAct = QtGui.QAction("Heads-Up-Display ", self)
self.hudAct.setShortcut("Shift+H")
self.hudAct.setCheckable(True)
self.hudAct.setChecked(self.camera.draw_hud)
self.connect(self.hudAct, QtCore.SIGNAL("toggled (bool)"),
self.camera._set_draw_hud)
options_menu.addAction(self.hudAct)
# labels toggle menu item
self.labelsAct = QtGui.QAction("Labels ", self)
self.labelsAct.setShortcut("Shift+L")
self.labelsAct.setCheckable(True)
self.labelsAct.setChecked(self.camera.draw_labels)
self.connect(self.labelsAct, QtCore.SIGNAL("toggled (bool)"),
self.camera._set_draw_labels)
options_menu.addAction(self.labelsAct)
# normals toggle menu item
self.normalsAct = QtGui.QAction("Normals ", self)
self.normalsAct.setShortcut("Shift+N")
self.normalsAct.setCheckable(True)
self.normalsAct.setChecked(self.camera.draw_normals)
self.connect(self.normalsAct, QtCore.SIGNAL("toggled (bool)"),
self.camera._set_draw_normals)
options_menu.addAction(self.normalsAct)
# bounds toggle menu item
self.boundsAct = QtGui.QAction("Scene Bounds ", self)
self.boundsAct.setShortcut("Shift+B")
self.boundsAct.setCheckable(True)
self.boundsAct.setChecked(self.camera.draw_bounds)
self.connect(self.boundsAct, QtCore.SIGNAL("toggled (bool)"),
self.camera._set_draw_bounds)
options_menu.addAction(self.boundsAct)
# grid toggle menu item
self.gridAct = QtGui.QAction("Show Grid ", self)
self.gridAct.setShortcut("Shift+G")
self.gridAct.setCheckable(True)
self.gridAct.setChecked(self.camera.draw_grid)
self.connect(self.gridAct, QtCore.SIGNAL("toggled (bool)"),
self.camera._set_draw_grid)
options_menu.addAction(self.gridAct)
# visibility toggle menu item
self.visibleAct = QtGui.QAction("Visible Only ", self)
self.visibleAct.setShortcut("Shift+V")
self.visibleAct.setCheckable(True)
self.visibleAct.setChecked(self.camera.visible)
self.connect(self.visibleAct, QtCore.SIGNAL("toggled (bool)"),
self.camera._set_visible)
options_menu.addAction(self.visibleAct)
# shading toggle menu item
self.shading_menu = QtGui.QMenu("Shading", self)
shading_group = QtGui.QActionGroup(self.shading_menu)
self.offAct = QtGui.QAction("Off", self)
self.offAct.setShortcut("0")
self.offAct.setCheckable(True)
self.offAct.setActionGroup(shading_group)
self.offAct.setData(Mode.OFF)
self.offAct.setChecked(self.camera.mode == Mode.OFF)
self.offAct.toggled.connect(self.handle_set_mode)
self.shading_menu.addAction(self.offAct)
self.fillAct = QtGui.QAction("Fill", self)
self.fillAct.setShortcut("1")
self.fillAct.setCheckable(True)
self.fillAct.setActionGroup(shading_group)
self.fillAct.setData(Mode.FILL)
self.fillAct.setChecked(self.camera.mode == Mode.FILL)
self.fillAct.toggled.connect(self.handle_set_mode)
self.shading_menu.addAction(self.fillAct)
self.lineAct = QtGui.QAction("Line", self)
self.lineAct.setShortcut("2")
self.lineAct.setCheckable(True)
self.lineAct.setActionGroup(shading_group)
self.lineAct.setData(Mode.LINE)
self.lineAct.setChecked(self.camera.mode == Mode.LINE)
self.lineAct.toggled.connect(self.handle_set_mode)
self.shading_menu.addAction(self.lineAct)
self.pointAct = QtGui.QAction("Point ", self)
self.pointAct.setShortcut("3")
self.pointAct.setCheckable(True)
self.pointAct.setActionGroup(shading_group)
self.pointAct.setData(Mode.POINT)
self.pointAct.setChecked(self.camera.mode == Mode.POINT)
self.pointAct.toggled.connect(self.handle_set_mode)
self.shading_menu.addAction(self.pointAct)
self.bboxAct = QtGui.QAction("Bounds ", self)
self.bboxAct.setShortcut("4")
self.bboxAct.setCheckable(True)
self.bboxAct.setActionGroup(shading_group)
self.bboxAct.setData(Mode.BOUNDS)
self.bboxAct.setChecked(self.camera.mode == Mode.BOUNDS)
self.bboxAct.toggled.connect(self.handle_set_mode)
self.shading_menu.addAction(self.bboxAct)
options_menu.addMenu(self.shading_menu)
menu.addMenu(options_menu)
menu.popup(QtCore.QPoint(event.globalX(), event.globalY()))
else:
self.__last_p2d = event.pos()
self.__last_pok, self.__last_p3d = self.map_to_sphere(self.__last_p2d)
@update_camera
[docs] def mouseMoveEvent(self, event):
"""
mouse move event handler
"""
# alt key is required to move the camera
if not event.modifiers() & QtCore.Qt.AltModifier:
return
newPoint2D = event.pos()
if ((newPoint2D.x() < 0) or (newPoint2D.x() > self.width()) or
(newPoint2D.y() < 0) or (newPoint2D.y() > self.height())):
return
value_y = 0
newPoint_hitSphere, newPoint3D = self.map_to_sphere(newPoint2D)
dx = float(newPoint2D.x() - self.__last_p2d.x())
dy = float(newPoint2D.y() - self.__last_p2d.y())
w = float(self.width())
h = float(self.height())
self.makeCurrent()
if (((event.buttons() & QtCore.Qt.LeftButton) and (event.buttons() & QtCore.Qt.MidButton))
or (event.buttons() & QtCore.Qt.LeftButton and event.modifiers() & QtCore.Qt.ControlModifier)
or (event.buttons() & QtCore.Qt.RightButton and event.modifiers() & QtCore.Qt.AltModifier)):
self.camera.dolly(dx, dy)
elif (event.buttons() & QtCore.Qt.MidButton
or (event.buttons() & QtCore.Qt.LeftButton and event.modifiers() & QtCore.Qt.ShiftModifier)):
self.camera.track(dx, dy)
elif event.buttons() & QtCore.Qt.LeftButton:
self.__rotating = True
self.camera.rotate(dx, dy)
# TODO: replace with global state (with these as attrs)
self.__last_p2d = newPoint2D
self.__last_p3d = newPoint3D
self.__last_pok = newPoint_hitSphere
[docs] def mouseReleaseEvent(self, event):
"""
mouse release event handler
"""
self.__rotating = False
self.__last_pok = False
super(GLWidget, self).mouseReleaseEvent(event)
@update_camera
[docs] def wheelEvent(self, event):
"""
mouse wheel event handler
"""
dx = float(event.delta()) / 10
self.camera.dolly(dx, 0)
event.accept()
if __name__ == "__main__":
if len(sys.argv) > 1:
filepath = sys.argv[1]
else:
filepath = None
create_viewer_app(filepath)