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

Reply via email to