Hi
As an exercise in learning Qt and PySide I'm attempting to port this
example:
http://labs.qt.nokia.com/2008/06/27/accelerate-your-widgets-with-opengl/
to Python
I mostly have it going but there are a few little issues outstanding.
1: The file dialog created by clicking "load model" is very broken.
2: I can't figure out where to import QFutureWatcher from so background
loading doesn't work.
3: There's something odd with the mouse interaction and animation, generally
after a mouse operation the animation stalls until the next mouse operation.
4: The line "statistics.layout().setMargin(20)" errors with "AttributeError:
'PySide.QtGui.QVBoxLayout' object has no attribute 'setMargin'"
I have attached what I have so far.
I would really appreciate it if someone could shed any light on these
issues.
G
from __future__ import division
import warnings
warnings.filterwarnings("error", module=__name__)
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtOpenGL import *
from OpenGL.GL import *
from OpenGL.GLU import *
QT_CONCURRENT = False
class Point3d(object):
def __init__(self, x=0, y=0, z=0):
self.x = x
self.y = y
self.z = z
def __add__(self, other):
return Point3d(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(self, other):
return Point3d(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(self, f):
return Point3d(self.x * f, self.y * f, self.z * f)
def normalize(self):
r = 1 / (self.x**2 + self.y**2 + self.z**2)**0.5
return Point3d(self.x * r, self.y * r, self.z * r)
def __getitem__(self, i):
return [self.x, self.y, self.z][i]
def __setitem__(self, i, v):
dat = [self.x, self.y, self.z]
dat[i] = v
self.x, self.y, self.z = dat
def dot(self, other):
return self.x * other.x + self.y * other.y + self.z * other.z
def cross(self, other):
return Point3d(self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x)
class Model(object):
def __init__(self, filePath):
self.fileName = QFileInfo(filePath).fileName()
self.points = []
self.edgeIndices = []
self.pointIndices = []
if filePath is None:
return
f = open(filePath)
boundsMin = Point3d( 1e9, 1e9, 1e9)
boundsMax = Point3d(-1e9,-1e9,-1e9)
for l in f:
if not l.strip() or l[0] == '#':
continue
s = l.split()
if s[0] == "v":
p = Point3d()
for i in range(3):
p[i] = float(s[i + 1])
boundsMin[i] = min(boundsMin[i], p[i])
boundsMax[i] = max(boundsMax[i], p[i])
self.points.append(p)
elif s[0] in ["f", "fo"]:
p = []
for vertex in s[1:]:
vertexIndex = int(vertex.split("/")[0])
if vertexIndex:
p.append(vertexIndex - 1 if vertexIndex > 0 else len(self.points) + vertexIndex)
for i in range(len(p)):
edgeA = p[i]
edgeB = p[(i + 1) % len(p)]
if edgeA < edgeB:
self.edgeIndices.extend([edgeA, edgeB])
for i in range(3):
self.pointIndices.append(p[i])
if len(p) == 4:
for i in range(3):
self.pointIndices.append(p[(i + 2) % 4])
bounds = boundsMax - boundsMin
scale = 1 / max(bounds.x, bounds.y, bounds.z)
for i in range(len(self.points)):
self.points[i] = (self.points[i] - (boundsMin + bounds * 0.5)) * scale
self.normals = [Point3d() for _ in self.points]
for i in range(0, len(self.pointIndices), 3):
a = self.points[self.pointIndices[i]];
b = self.points[self.pointIndices[i+1]];
c = self.points[self.pointIndices[i+2]];
normal = Point3d.cross(b - a, c - a).normalize()
for j in range(3):
self.normals[self.pointIndices[i + j]] += normal
self.normals = [p.normalize() for p in self.normals]
self.flat_normals = [i for p in self.normals for i in p]
self.flat_points = [i for p in self.points for i in p]
self.normal_lines = []
for p, n in zip(self.points, self.normals):
self.normal_lines.extend(p)
self.normal_lines.extend(p + n * 0.02)
def render(self, wireframe=False, normals=False):
glEnable(GL_DEPTH_TEST)
glEnableClientState(GL_VERTEX_ARRAY)
if wireframe:
glVertexPointer(3, GL_FLOAT, 0, self.flat_points)
glDrawElements(GL_LINES, len(self.edgeIndices), GL_UNSIGNED_INT, self.edgeIndices)
else:
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glEnable(GL_COLOR_MATERIAL)
glShadeModel(GL_SMOOTH)
glEnableClientState(GL_NORMAL_ARRAY)
glVertexPointer(3, GL_FLOAT, 0, self.flat_points)
glNormalPointer(GL_FLOAT, 0, self.flat_normals)
glDrawElements(GL_TRIANGLES, len(self.pointIndices), GL_UNSIGNED_INT, self.pointIndices)
glDisableClientState(GL_NORMAL_ARRAY)
glDisable(GL_COLOR_MATERIAL)
glDisable(GL_LIGHT0)
glDisable(GL_LIGHTING)
if normals:
glVertexPointer(3, GL_FLOAT, 0, self.normal_lines)
glDrawArrays(GL_LINES, 0, len(self.normals) * 2)
glDisableClientState(GL_VERTEX_ARRAY)
glDisable(GL_DEPTH_TEST)
def get_fileName(self):
return self.fileName
def get_faces(self):
return len(self.pointIndices) / 3
def get_edges(self):
return len(self.edgeIndices) / 2
def get_points(self):
return len(self.points)
class OpenGLScene(QGraphicsScene):
def __init__(self):
QGraphicsScene.__init__(self)
self.wireframeEnabled = False
self.normalsEnabled = False
self.modelColor = QColor(153, 255, 0)
self.backgroundColor = QColor(0, 170, 255)
self.model = Model(None)
self.time = QTime()
self.lastTime = 0
self.mouseEventTime = 0
self.distance = 1.4
self.rotation = Point3d()
self.angularMomentum = Point3d(0, 40, 0)
self.accumulatedMomentum = Point3d()
self.modelButton = QWidget()
if QT_CONCURRENT:
self.modelLoader = QFutureWatcher()
controls = self.createDialog("Controls")
self.modelButton = QPushButton("Load model")
self.modelButton.clicked.connect(self.loadModel)
if QT_CONCURRENT:
self.modelLoader.finished.connect(self.modelLoaded)
controls.layout().addWidget(self.modelButton)
wireframe = QCheckBox("Render as wireframe")
wireframe.toggled.connect(self.enableWireframe)
controls.layout().addWidget(wireframe)
normals = QCheckBox("Display normals vectors")
normals.toggled.connect(self.enableNormals)
controls.layout().addWidget(normals)
colorButton = QPushButton("Choose model color")
colorButton.clicked.connect(self.setModelColor)
controls.layout().addWidget(colorButton)
backgroundButton = QPushButton("Choose background color")
backgroundButton.clicked.connect(self.setBackgroundColor)
controls.layout().addWidget(backgroundButton)
statistics = self.createDialog("Model info")
#statistics.layout().setMargin(20)
self.labels = []
for i in range(4):
l = QLabel()
self.labels.append(l)
statistics.layout().addWidget(l)
instructions = self.createDialog("Instructions")
instructions.layout().addWidget(QLabel("Use mouse wheel to zoom model, and click and drag to rotate model"))
instructions.layout().addWidget(QLabel("Move the sun around to change the light position"))
widgets = [instructions, controls, statistics]
for w in widgets:
proxy = QGraphicsProxyWidget(None, Qt.Dialog)
proxy.setWidget(w)
self.addItem(proxy)
pos = QPointF(10, 10)
for item in self.items():
item.setFlag(QGraphicsItem.ItemIsMovable)
item.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
rect = item.boundingRect()
item.setPos(pos.x() - rect.x(), pos.y() - rect.y())
pos += QPointF(0, 10 + rect.height())
gradient = QRadialGradient(40, 40, 40, 40, 40)
gradient.setColorAt(0.2, Qt.yellow)
gradient.setColorAt(1, Qt.transparent)
self.lightItem = QGraphicsRectItem(0, 0, 80, 80)
self.lightItem.setPen(Qt.NoPen)
self.lightItem.setBrush(gradient)
self.lightItem.setFlag(QGraphicsItem.ItemIsMovable)
self.lightItem.setPos(800, 200)
self.addItem(self.lightItem)
self.loadModel("qt.obj")
self.time.start()
def drawBackground(self, painter, _):
if painter.paintEngine().type() not in [QPaintEngine.OpenGL2, QPaintEngine.OpenGL]:
print "OpenGLScene: drawBackground needs a QGLWidget to be set as viewport on the graphics view"
return
painter.beginNativePainting()
glClearColor(self.backgroundColor.redF(), self.backgroundColor.greenF(), self.backgroundColor.blueF(), 1)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
if self.model:
glMatrixMode(GL_PROJECTION)
glPushMatrix();
glLoadIdentity();
gluPerspective(70, self.width() / self.height(), 0.01, 1000)
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
pos = [self.lightItem.x() - self.width() / 2, self.height() / 2 - self.lightItem.y(), 512, 0]
glLightfv(GL_LIGHT0, GL_POSITION, pos)
glColor4f(self.modelColor.redF(), self.modelColor.greenF(), self.modelColor.blueF(), 1)
delta = self.time.elapsed() - self.lastTime
self.rotation += self.angularMomentum * (delta / 1000.0)
self.lastTime += delta
glTranslatef(0, 0, -self.distance)
glRotatef(self.rotation.x, 1, 0, 0)
glRotatef(self.rotation.y, 0, 1, 0)
glRotatef(self.rotation.z, 0, 0, 1)
glEnable(GL_MULTISAMPLE)
self.model.render(self.wireframeEnabled, self.normalsEnabled)
glDisable(GL_MULTISAMPLE)
glPopMatrix()
glMatrixMode(GL_PROJECTION)
glPopMatrix()
painter.endNativePainting()
QTimer.singleShot(20, self, self.update())
def enableWireframe(self, enabled):
self.wireframeEnabled = enabled
self.update()
def enableNormals(self, enabled):
self.normalsEnabled = enabled
self.update()
def setModelColor(self):
color = QColorDialog.getColor(self.modelColor)
if color.isValid():
self.modelColor = color
self.update()
def setBackgroundColor(self):
color = QColorDialog.getColor(self.backgroundColor)
if color.isValid():
self.backgroundColor = color
self.update()
def loadModel(self, filePath=None):
if filePath is None:
file_path = QFileDialog.getOpenFileName(None, "Choose model", "", "*.obj")
if not filePath:
return
self.modelButton.setEnabled(False)
QApplication.setOverrideCursor(Qt.BusyCursor)
if QT_CONCURRENT:
self.modelLoader.setFuture(QtConcurrent.run(self.loadModel, filePath))
else:
self.setModel(Model(filePath))
self.modelLoaded()
def modelLoaded(self):
if QT_CONCURRENT:
setModel(self.modelLoader.result())
self.modelButton.setEnabled(True)
QApplication.restoreOverrideCursor()
def mouseMoveEvent(self, event):
QGraphicsScene.mouseMoveEvent(self, event)
if event.isAccepted():
return
if event.buttons() & Qt.LeftButton:
delta = event.scenePos() - event.lastScenePos()
angularImpulse = Point3d(delta.y(), delta.x(), 0) * 0.1
self.rotation += angularImpulse
self.accumulatedMomentum += angularImpulse
event.accept()
self.update()
def mousePressEvent(self, event):
QGraphicsScene.mousePressEvent(self, event)
if event.isAccepted():
return
self.mouseEventTime = self.time.elapsed()
self.angularMomentum = self.accumulatedMomentum = Point3d()
event.accept()
def mouseReleaseEvent(self, event):
QGraphicsScene.mouseReleaseEvent(self, event)
if event.isAccepted():
return
delta = self.time.elapsed() - self.mouseEventTime
self.angularMomentum = self.accumulatedMomentum * (1000 / max(1, delta))
event.accept()
self.update()
def wheelEvent(self, event):
QGraphicsScene.wheelEvent(self, event)
if event.isAccepted():
return
self.distance *= 1.2 ** (-event.delta() / 120)
event.accept()
self.update()
def createDialog(self, windowTitle):
dialog = QDialog(None, Qt.CustomizeWindowHint | Qt.WindowTitleHint)
dialog.setWindowOpacity(0.8)
dialog.setWindowTitle(windowTitle)
dialog.setLayout(QVBoxLayout())
return dialog
def setModel(self, model):
self.model = model
self.labels[0].setText("File: %s" % self.model.get_fileName())
self.labels[1].setText("Points: %s" % self.model.get_points())
self.labels[2].setText("Edges: %s" % self.model.get_edges())
self.labels[3].setText("Faces: %s" % self.model.get_faces())
self.update()
class GraphicsView(QGraphicsView):
def __init__(self):
QGraphicsView.__init__(self)
self.setWindowTitle(self.tr("3D Model Viewer"))
def resizeEvent(self, event):
if self.scene():
self.scene().setSceneRect(QRect(QPoint(0, 0), event.size()))
QGraphicsView.resizeEvent(self, event)
def main(argv):
app = QApplication(sys.argv)
view = GraphicsView()
view.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers)))
view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
view.setScene(OpenGLScene())
view.show()
view.resize(1024, 768)
return app.exec_()
if __name__ == "__main__":
main(sys.argv)
_______________________________________________
PySide mailing list
[email protected]
http://lists.pyside.org/listinfo/pyside