From 1b38a61ebb5617a341039c0bb9b7695194f5a2c5 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Mon, 24 Jan 2022 15:05:05 +0100 Subject: Port the threadedqopenglwidget example Task-number: PYSIDE-841 Change-Id: Ia394ab350ab04281e2227dc3af950913f44c6564 Reviewed-by: Cristian Maureira-Fredes (cherry picked from commit 92631a253dd1baf17d5f0b2080c4a9c80263672d) Reviewed-by: Qt Cherry-pick Bot --- .../doc/threadedqopenglwidget.png | Bin 0 -> 10616 bytes .../doc/threadedqopenglwidget.rst | 9 + examples/opengl/threadedqopenglwidget/glwidget.py | 115 +++++++ examples/opengl/threadedqopenglwidget/main.py | 130 ++++++++ .../opengl/threadedqopenglwidget/mainwindow.py | 61 ++++ examples/opengl/threadedqopenglwidget/renderer.py | 363 +++++++++++++++++++++ .../threadedqopenglwidget.pyproject | 3 + 7 files changed, 681 insertions(+) create mode 100644 examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.png create mode 100644 examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.rst create mode 100644 examples/opengl/threadedqopenglwidget/glwidget.py create mode 100644 examples/opengl/threadedqopenglwidget/main.py create mode 100644 examples/opengl/threadedqopenglwidget/mainwindow.py create mode 100644 examples/opengl/threadedqopenglwidget/renderer.py create mode 100644 examples/opengl/threadedqopenglwidget/threadedqopenglwidget.pyproject diff --git a/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.png b/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.png new file mode 100644 index 000000000..263d7a3d1 Binary files /dev/null and b/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.png differ diff --git a/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.rst b/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.rst new file mode 100644 index 000000000..79e13cf60 --- /dev/null +++ b/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.rst @@ -0,0 +1,9 @@ +Threaded QOpenGLWidget Example +============================== + +The threaded QOpenGLWidget example demonstrates OpenGL rendering +in separate threads. + +.. image:: threadedqopenglwidget.png + :width: 400 + :alt: Threaded QOpenGLWidget Example Screenshot diff --git a/examples/opengl/threadedqopenglwidget/glwidget.py b/examples/opengl/threadedqopenglwidget/glwidget.py new file mode 100644 index 000000000..b9a1091c9 --- /dev/null +++ b/examples/opengl/threadedqopenglwidget/glwidget.py @@ -0,0 +1,115 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "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 The Qt Company Ltd nor the names of its +## 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." +## +## $QT_END_LICENSE$ +## +############################################################################# + + +from PySide6.QtOpenGLWidgets import QOpenGLWidget +from PySide6.QtCore import QMutexLocker, QSize, QThread, Slot, Signal + +from renderer import Renderer + + +class GLWidget(QOpenGLWidget): + + render_requested = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + + self.aboutToCompose.connect(self.on_about_to_compose) + self.frameSwapped.connect(self.on_frame_swapped) + self.aboutToResize.connect(self.on_about_to_resize) + self.resized.connect(self.on_resized) + + self._thread = QThread() + self._renderer = Renderer(self) + self._renderer.moveToThread(self._thread) + self._thread.finished.connect(self._renderer.deleteLater) + + self.render_requested.connect(self._renderer.render) + self._renderer.context_wanted.connect(self.grab_context) + + self._thread.start() + + def stop_rendering(self): + self._renderer.prepare_exit() + self._thread.quit() + self._thread.wait() + self._thread = None + self._renderer = None + + def closeEvent(self, event): + self.stop_rendering() + event.accept() + + def paintEvent(self, event): + pass + + def sizeHint(self): + return QSize(200, 200) + + @Slot() + def on_about_to_compose(self): + # We are on the gui thread here. Composition is about to + # begin. Wait until the render thread finishes. + self._renderer.lock_renderer() + + @Slot() + def on_frame_swapped(self): + self._renderer.unlock_renderer() + # Assuming a blocking swap, our animation is driven purely by the + # vsync in self example. + self.render_requested.emit() + + @Slot() + def on_about_to_resize(self): + self._renderer.lock_renderer() + + @Slot() + def on_resized(self): + self._renderer.unlock_renderer() + + def grab_context(self): + if not self._renderer: + return + self._renderer.lock_renderer() + with QMutexLocker(self._renderer.grab_mutex()): + self.context().moveToThread(self._thread) + self._renderer.grab_condition().wakeAll() + self._renderer.unlock_renderer() diff --git a/examples/opengl/threadedqopenglwidget/main.py b/examples/opengl/threadedqopenglwidget/main.py new file mode 100644 index 000000000..de8adc84a --- /dev/null +++ b/examples/opengl/threadedqopenglwidget/main.py @@ -0,0 +1,130 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "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 The Qt Company Ltd nor the names of its +## 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." +## +## $QT_END_LICENSE$ +## +############################################################################# + +"""PySide6 port of the Threaded QOpenGLWidget Example from Qt v6.x""" + +import sys + +from argparse import ArgumentParser, RawTextHelpFormatter + +from PySide6.QtWidgets import QApplication, QMessageBox +from PySide6.QtGui import QShortcut, QSurfaceFormat +from PySide6.QtCore import QCoreApplication, QPoint, qVersion, Qt + +try: + from OpenGL import GL +except ImportError: + app = QApplication(sys.argv) + message = "PyOpenGL must be installed to run this example." + message_box = QMessageBox(QMessageBox.Critical, + "Threaded QOpenGLWidget Example", + message, QMessageBox.Close) + detail = "Run:\npip install PyOpenGL PyOpenGL_accelerate" + message_box.setDetailedText(detail) + message_box.exec() + sys.exit(1) + +from glwidget import GLWidget +from mainwindow import MainWindow + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + desc = "Qt Threaded QOpenGLWidget Example" + parser = ArgumentParser(description=desc, + formatter_class=RawTextHelpFormatter) + parser.add_argument("--single", "-s", action="store_true", + help="Single thread") + options = parser.parse_args() + + QCoreApplication.setApplicationName(desc) + QCoreApplication.setOrganizationName("QtProject") + QCoreApplication.setApplicationVersion(qVersion()) + + format = QSurfaceFormat() + format.setDepthBufferSize(16) + QSurfaceFormat.setDefaultFormat(format) + + # Two top-level windows with two QOpenGLWidget children in each. The + # rendering for the four QOpenGLWidgets happens on four separate threads. + + top_gl_widget = GLWidget() + pos = top_gl_widget.screen().availableGeometry().topLeft() + pos += QPoint(200, 200) + top_gl_widget.setWindowTitle(desc + " top level") + top_gl_widget.move(pos) + top_gl_widget.show() + + functions = top_gl_widget.context().functions() + vendor = functions.glGetString(GL.GL_VENDOR) + renderer = functions.glGetString(GL.GL_RENDERER) + gl_info = f"{vendor}/f{renderer}" + + supports_threading = ("nouveau" not in gl_info and "ANGLE" not in gl_info + and "llvmpipe" not in gl_info) + tool_tip = gl_info + if not supports_threading: + tool_tip += "\ndoes not support threaded OpenGL." + top_gl_widget.setToolTip(tool_tip) + print(tool_tip) + + close_shortcut = QShortcut(Qt.CTRL | Qt.Key_Q, top_gl_widget) + close_shortcut.activated.connect(QApplication.closeAllWindows) + close_shortcut.setContext(Qt.ApplicationShortcut) + + mw1 = None + mw2 = None + + if not options.single and supports_threading: + pos += QPoint(100, 100) + mw1 = MainWindow() + mw1.setToolTip(tool_tip) + mw1.move(pos) + mw1.setWindowTitle(f"{desc} #1") + mw1.show() + pos += QPoint(100, 100) + mw2 = MainWindow() + mw2.setToolTip(tool_tip) + mw2.move(pos) + mw2.setWindowTitle(f"{desc} #2") + mw2.show() + + sys.exit(app.exec()) diff --git a/examples/opengl/threadedqopenglwidget/mainwindow.py b/examples/opengl/threadedqopenglwidget/mainwindow.py new file mode 100644 index 000000000..6159f4779 --- /dev/null +++ b/examples/opengl/threadedqopenglwidget/mainwindow.py @@ -0,0 +1,61 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "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 The Qt Company Ltd nor the names of its +## 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." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide6.QtWidgets import QWidget, QHBoxLayout + +from glwidget import GLWidget + + +class MainWindow(QWidget): + def __init__(self): + super().__init__() + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + self._glwidget1 = GLWidget(self) + layout.addWidget(self._glwidget1) + self._glwidget2 = GLWidget(self) + layout.addWidget(self._glwidget2) + + def closeEvent(self, event): + self._glwidget1.stop_rendering() + self._glwidget2.stop_rendering() + event.accept() diff --git a/examples/opengl/threadedqopenglwidget/renderer.py b/examples/opengl/threadedqopenglwidget/renderer.py new file mode 100644 index 000000000..45161048a --- /dev/null +++ b/examples/opengl/threadedqopenglwidget/renderer.py @@ -0,0 +1,363 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "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 The Qt Company Ltd nor the names of its +## 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." +## +## $QT_END_LICENSE$ +## +############################################################################# + +import ctypes +import math +import numpy + +from OpenGL import GL + +from PySide6.QtOpenGL import QOpenGLShader, QOpenGLShaderProgram, QOpenGLBuffer +from PySide6.QtGui import (QGuiApplication, QOpenGLFunctions, QVector3D, + QMatrix4x4) +from PySide6.QtCore import (QElapsedTimer, QObject, QMetaObject, QMutex, + QMutexLocker, QThread, QWaitCondition, Signal, Slot) + +# Some OpenGL implementations have serious issues with compiling and linking +# shaders on multiple threads concurrently. Avoid self. +init_mutex = QMutex() + + +VERTEX_SHADER = """attribute highp vec4 vertex; +attribute mediump vec3 normal; +uniform mediump mat4 matrix; +varying mediump vec4 color; +void main(void) +{ + vec3 toLight = normalize(vec3(0.0, 0.3, 1.0)); + float angle = max(dot(normal, toLight), 0.0); + vec3 col = vec3(0.40, 1.0, 0.0); + color = vec4(col * 0.2 + col * 0.8 * angle, 1.0); + color = clamp(color, 0.0, 1.0); + gl_Position = matrix * vertex; +} +""" + + +FRAGMENT_SHADER = """varying mediump vec4 color; +void main(void) +{ + gl_FragColor = color; +} +""" + + +class Renderer(QObject, QOpenGLFunctions): + + context_wanted = Signal() + + def __init__(self, widget): + QObject.__init__(self) + QOpenGLFunctions.__init__(self) + self._glwidget = widget + self._inited = False + self._fAngle = 0 + self._fScale = 1 + + self._vertices = [] + self._normals = [] + self._program = QOpenGLShaderProgram() + self._vbo = QOpenGLBuffer() + self._vertex_attr = 0 + self._normal_attr = 0 + self._matrix_uniform = 0 + self._renderMutex = QMutex() + self._elapsed = QElapsedTimer() + self._grabMutex = QMutex() + self._grab_condition = QWaitCondition() + self._exiting = False + + def lock_renderer(self): + self._renderMutex.lock() + + def unlock_renderer(self): + self._renderMutex.unlock() + + def grab_mutex(self): + return self._grabMutex + + def grab_condition(self): + return self._grab_condition + + def prepare_exit(self): + self._exiting = True + self._grab_condition.wakeAll() + + def paint_Qt_logo(self): + self._vbo.bind() + self._program.setAttributeBuffer(self._vertex_attr, GL.GL_FLOAT, 0, 3) + size = len(self._vertices) * 3 * ctypes.sizeof(ctypes.c_float) + self._program.setAttributeBuffer(self._normal_attr, GL.GL_FLOAT, size, 3) + self._vbo.release() + + self._program.enableAttributeArray(self._vertex_attr) + self._program.enableAttributeArray(self._normal_attr) + + self.glDrawArrays(GL.GL_TRIANGLES, 0, len(self._vertices)) + + self._program.disableAttributeArray(self._normal_attr) + self._program.disableAttributeArray(self._vertex_attr) + + @Slot() + def render(self): + global init_mutex + + if self._exiting: + return + + ctx = self._glwidget.context() + if not ctx: # QOpenGLWidget not yet initialized + return + + # Grab the context. + self._grabMutex.lock() + self.context_wanted.emit() + self._grab_condition.wait(self._grabMutex) + + with QMutexLocker(self._renderMutex): + self._grabMutex.unlock() + + if self._exiting: + return + + assert(ctx.thread() == QThread.currentThread()) + + # Make the context (and an offscreen surface) current for self thread. + # The QOpenGLWidget's fbo is bound in the context. + self._glwidget.makeCurrent() + + if not self._inited: + self._inited = True + self.initializeOpenGLFunctions() + with QMutexLocker(init_mutex): + self._init_gl() + self._elapsed.start() + + self._render_next() + + # Make no context current on self thread and move the + # QOpenGLWidget'scontext back to the gui thread. + self._glwidget.doneCurrent() + ctx.moveToThread(QGuiApplication.instance().thread()) + + # Schedule composition. Note that self will use QueuedConnection, + # meaning that update() will be invoked on the gui thread. + QMetaObject.invokeMethod(self._glwidget, "update") + + def _init_gl(self): + vshader = QOpenGLShader(QOpenGLShader.Vertex, self) + vshader.compileSourceCode(VERTEX_SHADER) + + fshader = QOpenGLShader(QOpenGLShader.Fragment, self) + fshader.compileSourceCode(FRAGMENT_SHADER) + + self._program.addShader(vshader) + self._program.addShader(fshader) + self._program.link() + + self._vertex_attr = self._program.attributeLocation("vertex") + self._normal_attr = self._program.attributeLocation("normal") + self._matrix_uniform = self._program.uniformLocation("matrix") + + self._fAngle = 0 + self._fScale = 1 + self.create_geometry() + + self._vbo.create() + self._vbo.bind() + + data_count = len(self._vertices) * 2 * 3 + data = numpy.empty(data_count, dtype=ctypes.c_float) + i = 0 + for v in self._vertices: + data[i] = v.x() + i += 1 + data[i] = v.y() + i += 1 + data[i] = v.z() + i += 1 + for n in self._normals: + data[i] = n.x() + i += 1 + data[i] = n.y() + i += 1 + data[i] = n.z() + i += 1 + + vertices_size = data_count * ctypes.sizeof(ctypes.c_float) + self._vbo.allocate(data.tobytes(), vertices_size) + + def _render_next(self): + self.glClearColor(0.1, 0.2, 0.2, 1.0) + self.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) + + self.glFrontFace(GL.GL_CW) + self.glCullFace(GL.GL_FRONT) + self.glEnable(GL.GL_CULL_FACE) + self.glEnable(GL.GL_DEPTH_TEST) + + modelview = QMatrix4x4() + modelview.rotate(self._fAngle, 0.0, 1.0, 0.0) + modelview.rotate(self._fAngle, 1.0, 0.0, 0.0) + modelview.rotate(self._fAngle, 0.0, 0.0, 1.0) + modelview.scale(self._fScale) + modelview.translate(0.0, -0.2, 0.0) + + self._program.bind() + self._program.setUniformValue(self._matrix_uniform, modelview) + self.paint_Qt_logo() + self._program.release() + + self.glDisable(GL.GL_DEPTH_TEST) + self.glDisable(GL.GL_CULL_FACE) + + self._fAngle += 1.0 + + def create_geometry(self): + self._vertices = [] + self._normals = [] + + x1 = +0.06 + y1 = -0.14 + x2 = +0.14 + y2 = -0.06 + x3 = +0.08 + y3 = +0.00 + x4 = +0.30 + y4 = +0.22 + + self.quad(x1, y1, x2, y2, y2, x2, y1, x1) + self.quad(x3, y3, x4, y4, y4, x4, y3, x3) + + self.extrude(x1, y1, x2, y2) + self.extrude(x2, y2, y2, x2) + self.extrude(y2, x2, y1, x1) + self.extrude(y1, x1, x1, y1) + self.extrude(x3, y3, x4, y4) + self.extrude(x4, y4, y4, x4) + self.extrude(y4, x4, y3, x3) + + NUM_SECTORS = 100 + SECTOR_ANGLE = 2 * math.pi / NUM_SECTORS + + for i in range(NUM_SECTORS): + angle = i * SECTOR_ANGLE + sin_angle = math.sin(angle) + cos_angle = math.cos(angle) + x5 = 0.30 * sin_angle + y5 = 0.30 * cos_angle + x6 = 0.20 * sin_angle + y6 = 0.20 * cos_angle + + angle += SECTOR_ANGLE + sin_angle = math.sin(angle) + cos_angle = math.cos(angle) + x7 = 0.20 * sin_angle + y7 = 0.20 * cos_angle + x8 = 0.30 * sin_angle + y8 = 0.30 * cos_angle + + self.quad(x5, y5, x6, y6, x7, y7, x8, y8) + + self.extrude(x6, y6, x7, y7) + self.extrude(x8, y8, x5, y5) + + for i in range(len(self._vertices)): + self._vertices[i] *= 2.0 + + def quad(self, x1, y1, x2, y2, x3, y3, x4, y4): + + self._vertices.append(QVector3D(x1, y1, -0.05)) + self._vertices.append(QVector3D(x2, y2, -0.05)) + self._vertices.append(QVector3D(x4, y4, -0.05)) + + self._vertices.append(QVector3D(x3, y3, -0.05)) + self._vertices.append(QVector3D(x4, y4, -0.05)) + self._vertices.append(QVector3D(x2, y2, -0.05)) + + n = QVector3D.normal(QVector3D(x2 - x1, y2 - y1, 0.0), + QVector3D(x4 - x1, y4 - y1, 0.0)) + + self._normals.append(n) + self._normals.append(n) + self._normals.append(n) + + self._normals.append(n) + self._normals.append(n) + self._normals.append(n) + + self._vertices.append(QVector3D(x4, y4, 0.05)) + self._vertices.append(QVector3D(x2, y2, 0.05)) + self._vertices.append(QVector3D(x1, y1, 0.05)) + + self._vertices.append(QVector3D(x2, y2, 0.05)) + self._vertices.append(QVector3D(x4, y4, 0.05)) + self._vertices.append(QVector3D(x3, y3, 0.05)) + + n = QVector3D.normal(QVector3D(x2 - x4, y2 - y4, 0.0), + QVector3D(x1 - x4, y1 - y4, 0.0)) + + self._normals.append(n) + self._normals.append(n) + self._normals.append(n) + + self._normals.append(n) + self._normals.append(n) + self._normals.append(n) + + def extrude(self, x1, y1, x2, y2): + self._vertices.append(QVector3D(x1, y1, +0.05)) + self._vertices.append(QVector3D(x2, y2, +0.05)) + self._vertices.append(QVector3D(x1, y1, -0.05)) + + self._vertices.append(QVector3D(x2, y2, -0.05)) + self._vertices.append(QVector3D(x1, y1, -0.05)) + self._vertices.append(QVector3D(x2, y2, +0.05)) + + n = QVector3D.normal(QVector3D(x2 - x1, y2 - y1, 0.0), + QVector3D(0.0, 0.0, -0.1)) + + self._normals.append(n) + self._normals.append(n) + self._normals.append(n) + + self._normals.append(n) + self._normals.append(n) + self._normals.append(n) diff --git a/examples/opengl/threadedqopenglwidget/threadedqopenglwidget.pyproject b/examples/opengl/threadedqopenglwidget/threadedqopenglwidget.pyproject new file mode 100644 index 000000000..3faba3113 --- /dev/null +++ b/examples/opengl/threadedqopenglwidget/threadedqopenglwidget.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "glwidget.py", "mainwindow.py", "renderer.py"] +} -- cgit v1.2.3