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 PySide@lists.pyside.org http://lists.pyside.org/listinfo/pyside