aboutsummaryrefslogtreecommitdiffstats
path: root/examples/opengl
diff options
context:
space:
mode:
Diffstat (limited to 'examples/opengl')
-rw-r--r--examples/opengl/contextinfo/contextinfo.py68
-rw-r--r--examples/opengl/hellogl2/doc/hellogl2.rst14
-rw-r--r--examples/opengl/hellogl2/glwidget.py272
-rw-r--r--examples/opengl/hellogl2/hellogl2.py489
-rw-r--r--examples/opengl/hellogl2/hellogl2.pyproject2
-rw-r--r--examples/opengl/hellogl2/logo.py101
-rw-r--r--examples/opengl/hellogl2/main.py58
-rw-r--r--examples/opengl/hellogl2/mainwindow.py29
-rw-r--r--examples/opengl/hellogl2/window.py110
-rw-r--r--examples/opengl/textures/textures.py214
-rw-r--r--examples/opengl/textures/textures.pyproject2
-rw-r--r--examples/opengl/textures/textures_rc.py550
-rw-r--r--examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.pngbin0 -> 10616 bytes
-rw-r--r--examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.rst9
-rw-r--r--examples/opengl/threadedqopenglwidget/glwidget.py79
-rw-r--r--examples/opengl/threadedqopenglwidget/main.py93
-rw-r--r--examples/opengl/threadedqopenglwidget/mainwindow.py24
-rw-r--r--examples/opengl/threadedqopenglwidget/renderer.py326
-rw-r--r--examples/opengl/threadedqopenglwidget/threadedqopenglwidget.pyproject3
19 files changed, 1510 insertions, 933 deletions
diff --git a/examples/opengl/contextinfo/contextinfo.py b/examples/opengl/contextinfo/contextinfo.py
index 73b55df13..311d5b765 100644
--- a/examples/opengl/contextinfo/contextinfo.py
+++ b/examples/opengl/contextinfo/contextinfo.py
@@ -1,43 +1,5 @@
-
-#############################################################################
-##
-## Copyright (C) 2017 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$
-##
-#############################################################################
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
"""PySide6 port of the opengl/contextinfo example from Qt v5.x"""
@@ -47,20 +9,20 @@ import sys
from textwrap import dedent
-from PySide6.QtCore import QCoreApplication, QLibraryInfo, QSize, QTimer, Qt
+from PySide6.QtCore import (QCoreApplication, QLibraryInfo, QSize, QTimer, Qt,
+ Slot)
from PySide6.QtGui import (QMatrix4x4, QOpenGLContext, QSurfaceFormat, QWindow)
from PySide6.QtOpenGL import (QOpenGLBuffer, QOpenGLShader,
QOpenGLShaderProgram, QOpenGLVertexArrayObject)
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QMessageBox, QPlainTextEdit,
- QWidget)
+ QWidget)
from PySide6.support import VoidPtr
try:
from OpenGL import GL
except ImportError:
app = QApplication(sys.argv)
message_box = QMessageBox(QMessageBox.Critical, "ContextInfo",
- "PyOpenGL must be installed to run this example.",
- QMessageBox.Close)
+ "PyOpenGL must be installed to run this example.", QMessageBox.Close)
message_box.setDetailedText("Run:\npip install PyOpenGL PyOpenGL_accelerate")
message_box.exec()
sys.exit(1)
@@ -111,7 +73,10 @@ colors = numpy.array([1, 0, 0, 0, 1, 0, 0, 0, 1], dtype=numpy.float32)
def print_surface_format(surface_format):
- profile_name = 'core' if surface_format.profile() == QSurfaceFormat.CoreProfile else 'compatibility'
+ if surface_format.profile() == QSurfaceFormat.CoreProfile:
+ profile_name = 'core'
+ else:
+ profile_name = 'compatibility'
major = surface_format.majorVersion()
minor = surface_format.minorVersion()
return f"{profile_name} version {major}.{minor}"
@@ -141,11 +106,13 @@ class RenderWindow(QWindow):
# concept 3.2+ has. This may still fail since version 150 (3.2) is
# specified in the sources but it's worth a try.
if (fmt.renderableType() == QSurfaceFormat.OpenGL and fmt.majorVersion() == 3
- and fmt.minorVersion() <= 1):
+ and fmt.minorVersion() <= 1):
use_new_style_shader = not fmt.testOption(QSurfaceFormat.DeprecatedFunctions)
vertex_shader = vertex_shader_source if use_new_style_shader else vertex_shader_source_110
- fragment_shader = fragment_shader_source if use_new_style_shader else fragment_shader_source_110
+ fragment_shader = (fragment_shader_source
+ if use_new_style_shader
+ else fragment_shader_source_110)
if not self.program.addShaderFromSourceCode(QOpenGLShader.Vertex, vertex_shader):
log = self.program.log()
raise Exception("Vertex shader could not be added: {log} ({vertexShader})")
@@ -170,9 +137,9 @@ class RenderWindow(QWindow):
self.vbo.write(vertices_size, VoidPtr(self._colors_data), colors_size)
self.vbo.release()
- vao_binder = QOpenGLVertexArrayObject.Binder(self.vao)
- if self.vao.isCreated(): # have VAO support, use it
- self.setup_vertex_attribs()
+ with QOpenGLVertexArrayObject.Binder(self.vao):
+ if self.vao.isCreated(): # have VAO support, use it
+ self.setup_vertex_attribs()
def setup_vertex_attribs(self):
self.vbo.bind()
@@ -230,6 +197,7 @@ class RenderWindow(QWindow):
self.context.swapBuffers(self)
self.context.doneCurrent()
+ @Slot()
def slot_timer(self):
self.render()
self.angle += 1
diff --git a/examples/opengl/hellogl2/doc/hellogl2.rst b/examples/opengl/hellogl2/doc/hellogl2.rst
index 1223e138c..3471ebf30 100644
--- a/examples/opengl/hellogl2/doc/hellogl2.rst
+++ b/examples/opengl/hellogl2/doc/hellogl2.rst
@@ -4,6 +4,20 @@ Hello GL2 Example
The Hello GL2 example demonstrates the basic use of the OpenGL-related classes
provided with Qt.
+In this example the widget's corresponding top-level window can change several
+times during the widget's lifetime. Whenever this happens, the QOpenGLWidget's
+associated context is destroyed and a new one is created, requiring us to clean
+up the GL resources.
+
+The equivalent C++ example does this cleanup on emission of the
+QOpenGLContext.aboutToBeDestroyed() signal. However, in Qt for Python, we
+cannot rely on this signal when it is emitted from the destructor.
+
+Therefore, we do the cleanup in GLWidget.hideEvent().
+
+This will be followed by an invocation of initializeGL() where we can recreate
+all resources.
+
.. image:: hellogl2.png
:width: 400
:alt: Hello GL2 Screenshot
diff --git a/examples/opengl/hellogl2/glwidget.py b/examples/opengl/hellogl2/glwidget.py
new file mode 100644
index 000000000..bbf200a6b
--- /dev/null
+++ b/examples/opengl/hellogl2/glwidget.py
@@ -0,0 +1,272 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# Copyright (C) 2013 Riverbank Computing Limited.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import ctypes
+from PySide6.QtCore import Signal, Slot, Qt, QSize, QPointF
+from PySide6.QtGui import (QVector3D, QOpenGLFunctions,
+ QMatrix4x4, QOpenGLContext, QSurfaceFormat)
+from PySide6.QtOpenGL import (QOpenGLVertexArrayObject, QOpenGLBuffer,
+ QOpenGLShaderProgram, QOpenGLShader)
+from PySide6.QtOpenGLWidgets import QOpenGLWidget
+
+from OpenGL import GL
+
+from shiboken6 import VoidPtr
+from logo import Logo
+
+FRAGMENT_SHADER_SOURCE_CORE = """#version 150
+in highp vec3 vert;
+in highp vec3 vertNormal;
+out highp vec4 fragColor;
+uniform highp vec3 lightPos;
+void main() {
+ highp vec3 L = normalize(lightPos - vert);
+ highp float NL = max(dot(normalize(vertNormal), L), 0.0);
+ highp vec3 color = vec3(0.39, 1.0, 0.0);
+ highp vec3 col = clamp(color * 0.2 + color * 0.8 * NL, 0.0, 1.0);
+ fragColor = vec4(col, 1.0);
+}"""
+
+
+FRAGMENT_SHADER_SOURCE = """varying highp vec3 vert;
+varying highp vec3 vertNormal;
+uniform highp vec3 lightPos;
+void main() {
+ highp vec3 L = normalize(lightPos - vert);
+ highp float NL = max(dot(normalize(vertNormal), L), 0.0);
+ highp vec3 color = vec3(0.39, 1.0, 0.0);
+ highp vec3 col = clamp(color * 0.2 + color * 0.8 * NL, 0.0, 1.0);
+ gl_FragColor = vec4(col, 1.0);
+}"""
+
+
+VERTEX_SHADER_SOURCE_CORE = """#version 150
+in vec4 vertex;
+in vec3 normal;
+out vec3 vert;
+out vec3 vertNormal;
+uniform mat4 projMatrix;
+uniform mat4 mvMatrix;
+uniform mat3 normalMatrix;
+void main() {
+ vert = vertex.xyz;
+ vertNormal = normalMatrix * normal;
+ gl_Position = projMatrix * mvMatrix * vertex;
+}"""
+
+
+VERTEX_SHADER_SOURCE = """attribute vec4 vertex;
+attribute vec3 normal;
+varying vec3 vert;
+varying vec3 vertNormal;
+uniform mat4 projMatrix;
+uniform mat4 mvMatrix;
+uniform mat3 normalMatrix;
+void main() {
+ vert = vertex.xyz;
+ vertNormal = normalMatrix * normal;
+ gl_Position = projMatrix * mvMatrix * vertex;
+}"""
+
+
+class GLWidget(QOpenGLWidget, QOpenGLFunctions):
+ x_rotation_changed = Signal(int)
+ y_rotation_changed = Signal(int)
+ z_rotation_changed = Signal(int)
+
+ _transparent = False
+
+ def __init__(self, parent=None):
+ QOpenGLWidget.__init__(self, parent)
+ QOpenGLFunctions.__init__(self)
+
+ self._core = QSurfaceFormat.defaultFormat().profile() == QSurfaceFormat.CoreProfile
+
+ self._x_rot = 0
+ self._y_rot = 0
+ self._z_rot = 0
+ self._last_pos = QPointF()
+ self.logo = Logo()
+ self.vao = QOpenGLVertexArrayObject()
+ self._logo_vbo = QOpenGLBuffer()
+ self.program = QOpenGLShaderProgram()
+ self._proj_matrix_loc = 0
+ self._mv_matrix_loc = 0
+ self._normal_matrix_loc = 0
+ self._light_pos_loc = 0
+ self.proj = QMatrix4x4()
+ self.camera = QMatrix4x4()
+ self.world = QMatrix4x4()
+ if self._transparent:
+ fmt = self.format()
+ fmt.setAlphaBufferSize(8)
+ self.setFormat(fmt)
+
+ @staticmethod
+ def set_transparent(t):
+ GLWidget._transparent = t
+
+ @staticmethod
+ def is_transparent():
+ return GLWidget._transparent
+
+ def x_rotation(self):
+ return self._x_rot
+
+ def y_rotation(self):
+ return self._y_rot
+
+ def z_rotation(self):
+ return self._z_rot
+
+ def minimumSizeHint(self):
+ return QSize(50, 50)
+
+ def sizeHint(self):
+ return QSize(400, 400)
+
+ def normalize_angle(self, angle):
+ while angle < 0:
+ angle += 360 * 16
+ while angle > 360 * 16:
+ angle -= 360 * 16
+ return angle
+
+ @Slot(int)
+ def set_xrotation(self, angle):
+ angle = self.normalize_angle(angle)
+ if angle != self._x_rot:
+ self._x_rot = angle
+ self.x_rotation_changed.emit(angle)
+ self.update()
+
+ @Slot(int)
+ def set_yrotation(self, angle):
+ angle = self.normalize_angle(angle)
+ if angle != self._y_rot:
+ self._y_rot = angle
+ self.y_rotation_changed.emit(angle)
+ self.update()
+
+ @Slot(int)
+ def set_zrotation(self, angle):
+ angle = self.normalize_angle(angle)
+ if angle != self._z_rot:
+ self._z_rot = angle
+ self.z_rotation_changed.emit(angle)
+ self.update()
+
+ @Slot()
+ def cleanup(self):
+ if self.program:
+ self.makeCurrent()
+ self._logo_vbo.destroy()
+ del self.program
+ self.program = None
+ self.doneCurrent()
+
+ def initializeGL(self):
+ self.initializeOpenGLFunctions()
+ self.glClearColor(0, 0, 0, 0 if self._transparent else 1)
+
+ self.program = QOpenGLShaderProgram()
+
+ if self._core:
+ self._vertex_shader = VERTEX_SHADER_SOURCE_CORE
+ self._fragment_shader = FRAGMENT_SHADER_SOURCE_CORE
+ else:
+ self._vertex_shader = VERTEX_SHADER_SOURCE
+ self._fragment_shader = FRAGMENT_SHADER_SOURCE
+
+ self.program.addShaderFromSourceCode(QOpenGLShader.Vertex,
+ self._vertex_shader)
+ self.program.addShaderFromSourceCode(QOpenGLShader.Fragment,
+ self._fragment_shader)
+ self.program.bindAttributeLocation("vertex", 0)
+ self.program.bindAttributeLocation("normal", 1)
+ self.program.link()
+
+ self.program.bind()
+ self._proj_matrix_loc = self.program.uniformLocation("projMatrix")
+ self._mv_matrix_loc = self.program.uniformLocation("mvMatrix")
+ self._normal_matrix_loc = self.program.uniformLocation("normalMatrix")
+ self._light_pos_loc = self.program.uniformLocation("lightPos")
+
+ self.vao.create()
+ with QOpenGLVertexArrayObject.Binder(self.vao):
+ self._logo_vbo.create()
+ self._logo_vbo.bind()
+ float_size = ctypes.sizeof(ctypes.c_float)
+ self._logo_vbo.allocate(self.logo.const_data(),
+ self.logo.count() * float_size)
+
+ self.setup_vertex_attribs()
+
+ self.camera.setToIdentity()
+ self.camera.translate(0, 0, -1)
+
+ self.program.setUniformValue(self._light_pos_loc,
+ QVector3D(0, 0, 70))
+ self.program.release()
+
+ def setup_vertex_attribs(self):
+ self._logo_vbo.bind()
+ f = QOpenGLContext.currentContext().functions()
+ f.glEnableVertexAttribArray(0)
+ f.glEnableVertexAttribArray(1)
+ float_size = ctypes.sizeof(ctypes.c_float)
+
+ null = VoidPtr(0)
+ pointer = VoidPtr(3 * float_size)
+ f.glVertexAttribPointer(0, 3, int(GL.GL_FLOAT), int(GL.GL_FALSE),
+ 6 * float_size, null)
+ f.glVertexAttribPointer(1, 3, int(GL.GL_FLOAT), int(GL.GL_FALSE),
+ 6 * float_size, pointer)
+ self._logo_vbo.release()
+
+ def paintGL(self):
+ self.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
+ self.glEnable(GL.GL_DEPTH_TEST)
+ self.glEnable(GL.GL_CULL_FACE)
+
+ self.world.setToIdentity()
+ self.world.rotate(180 - (self._x_rot / 16), 1, 0, 0)
+ self.world.rotate(self._y_rot / 16, 0, 1, 0)
+ self.world.rotate(self._z_rot / 16, 0, 0, 1)
+
+ with QOpenGLVertexArrayObject.Binder(self.vao):
+ self.program.bind()
+ self.program.setUniformValue(self._proj_matrix_loc, self.proj)
+ self.program.setUniformValue(self._mv_matrix_loc,
+ self.camera * self.world)
+ normal_matrix = self.world.normalMatrix()
+ self.program.setUniformValue(self._normal_matrix_loc, normal_matrix)
+
+ self.glDrawArrays(GL.GL_TRIANGLES, 0, self.logo.vertex_count())
+ self.program.release()
+
+ def resizeGL(self, width, height):
+ self.proj.setToIdentity()
+ self.proj.perspective(45, width / height, 0.01, 100)
+
+ def hideEvent(self, event):
+ self.cleanup()
+ super().hideEvent(event)
+
+ def mousePressEvent(self, event):
+ self._last_pos = event.position()
+
+ def mouseMoveEvent(self, event):
+ pos = event.position()
+ dx = pos.x() - self._last_pos.x()
+ dy = pos.y() - self._last_pos.y()
+
+ if event.buttons() & Qt.LeftButton:
+ self.set_xrotation(self._x_rot + 8 * dy)
+ self.set_yrotation(self._y_rot + 8 * dx)
+ elif event.buttons() & Qt.RightButton:
+ self.set_xrotation(self._x_rot + 8 * dy)
+ self.set_zrotation(self._z_rot + 8 * dx)
+
+ self._last_pos = pos
diff --git a/examples/opengl/hellogl2/hellogl2.py b/examples/opengl/hellogl2/hellogl2.py
deleted file mode 100644
index 2f170dc90..000000000
--- a/examples/opengl/hellogl2/hellogl2.py
+++ /dev/null
@@ -1,489 +0,0 @@
-
-############################################################################
-##
-## Copyright (C) 2013 Riverbank Computing Limited.
-## Copyright (C) 2021 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 opengl/hellogl2 example from Qt v5.x"""
-
-from argparse import ArgumentParser, RawTextHelpFormatter
-import ctypes
-import math
-import numpy
-import sys
-from PySide6.QtCore import QCoreApplication, Signal, SIGNAL, SLOT, Qt, QSize, QPointF
-from PySide6.QtGui import (QVector3D, QOpenGLFunctions,
- QMatrix4x4, QOpenGLContext, QSurfaceFormat)
-from PySide6.QtOpenGL import (QOpenGLVertexArrayObject, QOpenGLBuffer,
- QOpenGLShaderProgram, QOpenGLShader)
-from PySide6.QtWidgets import (QApplication, QWidget, QMessageBox, QHBoxLayout,
- QSlider)
-from PySide6.QtOpenGLWidgets import QOpenGLWidget
-
-from shiboken6 import VoidPtr
-
-try:
- from OpenGL import GL
-except ImportError:
- app = QApplication(sys.argv)
- message_box = QMessageBox(QMessageBox.Critical, "OpenGL hellogl",
- "PyOpenGL must be installed to run this example.",
- QMessageBox.Close)
- message_box.setDetailedText("Run:\npip install PyOpenGL PyOpenGL_accelerate")
- message_box.exec()
- sys.exit(1)
-
-
-class Window(QWidget):
- def __init__(self, transparent, parent=None):
- QWidget.__init__(self, parent)
-
- if transparent:
- self.setAttribute(Qt.WA_TranslucentBackground)
- self.setAttribute(Qt.WA_NoSystemBackground, False)
-
- self._gl_widget = GLWidget(transparent)
-
- self._x_slider = self.create_slider()
- self._x_slider.valueChanged.connect(self._gl_widget.set_xrotation)
- self._gl_widget.x_rotation_changed.connect(self._x_slider.setValue)
-
- self._y_slider = self.create_slider()
- self._y_slider.valueChanged.connect(self._gl_widget.set_yrotation)
- self._gl_widget.y_rotation_changed.connect(self._y_slider.setValue)
-
- self._z_slider = self.create_slider()
- self._z_slider.valueChanged.connect(self._gl_widget.set_zrotation)
- self._gl_widget.z_rotation_changed.connect(self._z_slider.setValue)
-
- main_layout = QHBoxLayout()
- main_layout.addWidget(self._gl_widget)
- main_layout.addWidget(self._x_slider)
- main_layout.addWidget(self._y_slider)
- main_layout.addWidget(self._z_slider)
- self.setLayout(main_layout)
-
- self._x_slider.setValue(15 * 16)
- self._y_slider.setValue(345 * 16)
- self._z_slider.setValue(0 * 16)
-
- self.setWindowTitle(self.tr("Hello GL"))
-
- def create_slider(self):
- slider = QSlider(Qt.Vertical)
-
- slider.setRange(0, 360 * 16)
- slider.setSingleStep(16)
- slider.setPageStep(15 * 16)
- slider.setTickInterval(15 * 16)
- slider.setTickPosition(QSlider.TicksRight)
- return slider
-
- def keyPressEvent(self, event):
- if event.key() == Qt.Key_Escape:
- self.close()
- else:
- super(Window, self).keyPressEvent(event)
-
-
-class Logo():
- def __init__(self):
- self.m_count = 0
- self.i = 0
- self.m_data = numpy.empty(2500 * 6, dtype=ctypes.c_float)
-
- 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
-
- for i in range(NUM_SECTORS):
- angle = (i * 2 * math.pi) / NUM_SECTORS
- x5 = 0.30 * math.sin(angle)
- y5 = 0.30 * math.cos(angle)
- x6 = 0.20 * math.sin(angle)
- y6 = 0.20 * math.cos(angle)
-
- angle = ((i + 1) * 2 * math.pi) / NUM_SECTORS
- x7 = 0.20 * math.sin(angle)
- y7 = 0.20 * math.cos(angle)
- x8 = 0.30 * math.sin(angle)
- y8 = 0.30 * math.cos(angle)
-
- self.quad(x5, y5, x6, y6, x7, y7, x8, y8)
-
- self.extrude(x6, y6, x7, y7)
- self.extrude(x8, y8, x5, y5)
-
- def const_data(self):
- return self.m_data.tobytes()
-
- def count(self):
- return self.m_count
-
- def vertex_count(self):
- return self.m_count / 6
-
- def quad(self, x1, y1, x2, y2, x3, y3, x4, y4):
- n = QVector3D.normal(QVector3D(x4 - x1, y4 - y1, 0), QVector3D(x2 - x1, y2 - y1, 0))
-
- self.add(QVector3D(x1, y1, -0.05), n)
- self.add(QVector3D(x4, y4, -0.05), n)
- self.add(QVector3D(x2, y2, -0.05), n)
-
- self.add(QVector3D(x3, y3, -0.05), n)
- self.add(QVector3D(x2, y2, -0.05), n)
- self.add(QVector3D(x4, y4, -0.05), n)
-
- n = QVector3D.normal(QVector3D(x1 - x4, y1 - y4, 0), QVector3D(x2 - x4, y2 - y4, 0))
-
- self.add(QVector3D(x4, y4, 0.05), n)
- self.add(QVector3D(x1, y1, 0.05), n)
- self.add(QVector3D(x2, y2, 0.05), n)
-
- self.add(QVector3D(x2, y2, 0.05), n)
- self.add(QVector3D(x3, y3, 0.05), n)
- self.add(QVector3D(x4, y4, 0.05), n)
-
- def extrude(self, x1, y1, x2, y2):
- n = QVector3D.normal(QVector3D(0, 0, -0.1), QVector3D(x2 - x1, y2 - y1, 0))
-
- self.add(QVector3D(x1, y1, 0.05), n)
- self.add(QVector3D(x1, y1, -0.05), n)
- self.add(QVector3D(x2, y2, 0.05), n)
-
- self.add(QVector3D(x2, y2, -0.05), n)
- self.add(QVector3D(x2, y2, 0.05), n)
- self.add(QVector3D(x1, y1, -0.05), n)
-
- def add(self, v, n):
- self.m_data[self.i] = v.x()
- self.i += 1
- self.m_data[self.i] = v.y()
- self.i += 1
- self.m_data[self.i] = v.z()
- self.i += 1
- self.m_data[self.i] = n.x()
- self.i += 1
- self.m_data[self.i] = n.y()
- self.i += 1
- self.m_data[self.i] = n.z()
- self.i += 1
- self.m_count += 6
-
-
-class GLWidget(QOpenGLWidget, QOpenGLFunctions):
- x_rotation_changed = Signal(int)
- y_rotation_changed = Signal(int)
- z_rotation_changed = Signal(int)
-
- def __init__(self, transparent, parent=None):
- QOpenGLWidget.__init__(self, parent)
- QOpenGLFunctions.__init__(self)
-
- self._transparent = transparent
- self._core = QSurfaceFormat.defaultFormat().profile() == QSurfaceFormat.CoreProfile
-
- self._x_rot = 0
- self._y_rot = 0
- self._z_rot = 0
- self._last_pos = QPointF()
- self.logo = Logo()
- self.vao = QOpenGLVertexArrayObject()
- self._logo_vbo = QOpenGLBuffer()
- self.program = QOpenGLShaderProgram()
- self._proj_matrix_loc = 0
- self._mv_matrix_loc = 0
- self._normal_matrix_loc = 0
- self._light_pos_loc = 0
- self.proj = QMatrix4x4()
- self.camera = QMatrix4x4()
- self.world = QMatrix4x4()
- if transparent:
- fmt = self.format()
- fmt.setAlphaBufferSize(8)
- self.setFormat(fmt)
-
- def x_rotation(self):
- return self._x_rot
-
- def y_rotation(self):
- return self._y_rot
-
- def z_rotation(self):
- return self._z_rot
-
- def minimumSizeHint(self):
- return QSize(50, 50)
-
- def sizeHint(self):
- return QSize(400, 400)
-
- def normalize_angle(self, angle):
- while angle < 0:
- angle += 360 * 16
- while angle > 360 * 16:
- angle -= 360 * 16
- return angle
-
- def set_xrotation(self, angle):
- angle = self.normalize_angle(angle)
- if angle != self._x_rot:
- self._x_rot = angle
- self.x_rotation_changed.emit(angle)
- self.update()
-
- def set_yrotation(self, angle):
- angle = self.normalize_angle(angle)
- if angle != self._y_rot:
- self._y_rot = angle
- self.y_rotation_changed.emit(angle)
- self.update()
-
- def set_zrotation(self, angle):
- angle = self.normalize_angle(angle)
- if angle != self._z_rot:
- self._z_rot = angle
- self.z_rotation_changed.emit(angle)
- self.update()
-
- def cleanup(self):
- self.makeCurrent()
- self._logo_vbo.destroy()
- del self.program
- self.program = None
- self.doneCurrent()
-
- def vertex_shader_source_core(self):
- return """#version 150
- in vec4 vertex;
- in vec3 normal;
- out vec3 vert;
- out vec3 vertNormal;
- uniform mat4 projMatrix;
- uniform mat4 mvMatrix;
- uniform mat3 normalMatrix;
- void main() {
- vert = vertex.xyz;
- vertNormal = normalMatrix * normal;
- gl_Position = projMatrix * mvMatrix * vertex;
- }"""
-
- def fragment_shader_source_core(self):
- return """#version 150
- in highp vec3 vert;
- in highp vec3 vertNormal;
- out highp vec4 fragColor;
- uniform highp vec3 lightPos;
- void main() {
- highp vec3 L = normalize(lightPos - vert);
- highp float NL = max(dot(normalize(vertNormal), L), 0.0);
- highp vec3 color = vec3(0.39, 1.0, 0.0);
- highp vec3 col = clamp(color * 0.2 + color * 0.8 * NL, 0.0, 1.0);
- fragColor = vec4(col, 1.0);
- }"""
-
- def vertex_shader_source(self):
- return """attribute vec4 vertex;
- attribute vec3 normal;
- varying vec3 vert;
- varying vec3 vertNormal;
- uniform mat4 projMatrix;
- uniform mat4 mvMatrix;
- uniform mat3 normalMatrix;
- void main() {
- vert = vertex.xyz;
- vertNormal = normalMatrix * normal;
- gl_Position = projMatrix * mvMatrix * vertex;
- }"""
-
- def fragment_shader_source(self):
- return """varying highp vec3 vert;
- varying highp vec3 vertNormal;
- uniform highp vec3 lightPos;
- void main() {
- highp vec3 L = normalize(lightPos - vert);
- highp float NL = max(dot(normalize(vertNormal), L), 0.0);
- highp vec3 color = vec3(0.39, 1.0, 0.0);
- highp vec3 col = clamp(color * 0.2 + color * 0.8 * NL, 0.0, 1.0);
- gl_FragColor = vec4(col, 1.0);
- }"""
-
- def initializeGL(self):
- self.context().aboutToBeDestroyed.connect(self.cleanup)
- self.initializeOpenGLFunctions()
- self.glClearColor(0, 0, 0, 0 if self._transparent else 1)
-
- self.program = QOpenGLShaderProgram()
-
- if self._core:
- self._vertex_shader = self.vertex_shader_source_core()
- self._fragment_shader = self.fragment_shader_source_core()
- else:
- self._vertex_shader = self.vertex_shader_source()
- self._fragment_shader = self.fragment_shader_source()
-
- self.program.addShaderFromSourceCode(QOpenGLShader.Vertex, self._vertex_shader)
- self.program.addShaderFromSourceCode(QOpenGLShader.Fragment, self._fragment_shader)
- self.program.bindAttributeLocation("vertex", 0)
- self.program.bindAttributeLocation("normal", 1)
- self.program.link()
-
- self.program.bind()
- self._proj_matrix_loc = self.program.uniformLocation("projMatrix")
- self._mv_matrix_loc = self.program.uniformLocation("mvMatrix")
- self._normal_matrix_loc = self.program.uniformLocation("normalMatrix")
- self._light_pos_loc = self.program.uniformLocation("lightPos")
-
- self.vao.create()
- vao_binder = QOpenGLVertexArrayObject.Binder(self.vao)
-
- self._logo_vbo.create()
- self._logo_vbo.bind()
- float_size = ctypes.sizeof(ctypes.c_float)
- self._logo_vbo.allocate(self.logo.const_data(), self.logo.count() * float_size)
-
- self.setup_vertex_attribs()
-
- self.camera.setToIdentity()
- self.camera.translate(0, 0, -1)
-
- self.program.setUniformValue(self._light_pos_loc, QVector3D(0, 0, 70))
- self.program.release()
- vao_binder = None
-
- def setup_vertex_attribs(self):
- self._logo_vbo.bind()
- f = QOpenGLContext.currentContext().functions()
- f.glEnableVertexAttribArray(0)
- f.glEnableVertexAttribArray(1)
- float_size = ctypes.sizeof(ctypes.c_float)
-
- null = VoidPtr(0)
- pointer = VoidPtr(3 * float_size)
- f.glVertexAttribPointer(0, 3, int(GL.GL_FLOAT), int(GL.GL_FALSE), 6 * float_size, null)
- f.glVertexAttribPointer(1, 3, int(GL.GL_FLOAT), int(GL.GL_FALSE), 6 * float_size, pointer)
- self._logo_vbo.release()
-
- def paintGL(self):
- self.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
- self.glEnable(GL.GL_DEPTH_TEST)
- self.glEnable(GL.GL_CULL_FACE)
-
- self.world.setToIdentity()
- self.world.rotate(180 - (self._x_rot / 16), 1, 0, 0)
- self.world.rotate(self._y_rot / 16, 0, 1, 0)
- self.world.rotate(self._z_rot / 16, 0, 0, 1)
-
- vao_binder = QOpenGLVertexArrayObject.Binder(self.vao)
- self.program.bind()
- self.program.setUniformValue(self._proj_matrix_loc, self.proj)
- self.program.setUniformValue(self._mv_matrix_loc, self.camera * self.world)
- normal_matrix = self.world.normalMatrix()
- self.program.setUniformValue(self._normal_matrix_loc, normal_matrix)
-
- self.glDrawArrays(GL.GL_TRIANGLES, 0, self.logo.vertex_count())
- self.program.release()
- vao_binder = None
-
- def resizeGL(self, width, height):
- self.proj.setToIdentity()
- self.proj.perspective(45, width / height, 0.01, 100)
-
- def mousePressEvent(self, event):
- self._last_pos = event.position()
-
- def mouseMoveEvent(self, event):
- pos = event.position()
- dx = pos.x() - self._last_pos.x()
- dy = pos.y() - self._last_pos.y()
-
- if event.buttons() & Qt.LeftButton:
- self.set_xrotation(self._x_rot + 8 * dy)
- self.set_yrotation(self._y_rot + 8 * dx)
- elif event.buttons() & Qt.RightButton:
- self.set_xrotation(self._x_rot + 8 * dy)
- self.set_zrotation(self._z_rot + 8 * dx)
-
- self._last_pos = pos
-
-
-if __name__ == '__main__':
- app = QApplication(sys.argv)
- parser = ArgumentParser(description="hellogl2", formatter_class=RawTextHelpFormatter)
- parser.add_argument('--multisample', '-m', action='store_true',
- help='Use Multisampling')
- parser.add_argument('--coreprofile', '-c', action='store_true',
- help='Use Core Profile')
- parser.add_argument('--transparent', '-t', action='store_true',
- help='Transparent Windows')
- options = parser.parse_args()
-
- fmt = QSurfaceFormat()
- fmt.setDepthBufferSize(24)
- if options.multisample:
- fmt.setSamples(4)
- if options.coreprofile:
- fmt.setVersion(3, 2)
- fmt.setProfile(QSurfaceFormat.CoreProfile)
- QSurfaceFormat.setDefaultFormat(fmt)
-
- main_window = Window(options.transparent)
- main_window.resize(main_window.sizeHint())
- main_window.show()
-
- res = app.exec()
- sys.exit(res)
diff --git a/examples/opengl/hellogl2/hellogl2.pyproject b/examples/opengl/hellogl2/hellogl2.pyproject
index 331d835af..d85a139e4 100644
--- a/examples/opengl/hellogl2/hellogl2.pyproject
+++ b/examples/opengl/hellogl2/hellogl2.pyproject
@@ -1,3 +1,3 @@
{
- "files": ["hellogl2.py"]
+ "files": ["main.py", "glwidget.py", "logo.py", "mainwindow.py", "window.py"]
}
diff --git a/examples/opengl/hellogl2/logo.py b/examples/opengl/hellogl2/logo.py
new file mode 100644
index 000000000..c236a1ec9
--- /dev/null
+++ b/examples/opengl/hellogl2/logo.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# Copyright (C) 2013 Riverbank Computing Limited.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import math
+
+from PySide6.QtGui import (QVector3D, QVector3DList)
+
+
+class Logo():
+ def __init__(self):
+ self.m_data = QVector3DList()
+ self.m_data.reserve(5000)
+
+ 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
+
+ for i in range(NUM_SECTORS):
+ angle = (i * 2 * math.pi) / NUM_SECTORS
+ x5 = 0.30 * math.sin(angle)
+ y5 = 0.30 * math.cos(angle)
+ x6 = 0.20 * math.sin(angle)
+ y6 = 0.20 * math.cos(angle)
+
+ angle = ((i + 1) * 2 * math.pi) / NUM_SECTORS
+ x7 = 0.20 * math.sin(angle)
+ y7 = 0.20 * math.cos(angle)
+ x8 = 0.30 * math.sin(angle)
+ y8 = 0.30 * math.cos(angle)
+
+ self.quad(x5, y5, x6, y6, x7, y7, x8, y8)
+
+ self.extrude(x6, y6, x7, y7)
+ self.extrude(x8, y8, x5, y5)
+
+ def const_data(self):
+ return self.m_data.constData()
+
+ def count(self):
+ return len(self.m_data) * 3
+
+ def vertex_count(self):
+ return self.count() / 6
+
+ def quad(self, x1, y1, x2, y2, x3, y3, x4, y4):
+ n = QVector3D.normal(QVector3D(x4 - x1, y4 - y1, 0),
+ QVector3D(x2 - x1, y2 - y1, 0))
+
+ self.add(QVector3D(x1, y1, -0.05), n)
+ self.add(QVector3D(x4, y4, -0.05), n)
+ self.add(QVector3D(x2, y2, -0.05), n)
+
+ self.add(QVector3D(x3, y3, -0.05), n)
+ self.add(QVector3D(x2, y2, -0.05), n)
+ self.add(QVector3D(x4, y4, -0.05), n)
+
+ n = QVector3D.normal(QVector3D(x1 - x4, y1 - y4, 0),
+ QVector3D(x2 - x4, y2 - y4, 0))
+
+ self.add(QVector3D(x4, y4, 0.05), n)
+ self.add(QVector3D(x1, y1, 0.05), n)
+ self.add(QVector3D(x2, y2, 0.05), n)
+
+ self.add(QVector3D(x2, y2, 0.05), n)
+ self.add(QVector3D(x3, y3, 0.05), n)
+ self.add(QVector3D(x4, y4, 0.05), n)
+
+ def extrude(self, x1, y1, x2, y2):
+ n = QVector3D.normal(QVector3D(0, 0, -0.1),
+ QVector3D(x2 - x1, y2 - y1, 0))
+
+ self.add(QVector3D(x1, y1, 0.05), n)
+ self.add(QVector3D(x1, y1, -0.05), n)
+ self.add(QVector3D(x2, y2, 0.05), n)
+
+ self.add(QVector3D(x2, y2, -0.05), n)
+ self.add(QVector3D(x2, y2, 0.05), n)
+ self.add(QVector3D(x1, y1, -0.05), n)
+
+ def add(self, v, n):
+ self.m_data.append(v)
+ self.m_data.append(n)
diff --git a/examples/opengl/hellogl2/main.py b/examples/opengl/hellogl2/main.py
new file mode 100644
index 000000000..c7eb78a82
--- /dev/null
+++ b/examples/opengl/hellogl2/main.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# Copyright (C) 2013 Riverbank Computing Limited.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+"""PySide6 port of the opengl/hellogl2 example from Qt v6.x"""
+
+from argparse import ArgumentParser, RawTextHelpFormatter
+import sys
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QSurfaceFormat
+from PySide6.QtWidgets import (QApplication, QMessageBox)
+
+
+try:
+ from mainwindow import MainWindow
+ from glwidget import GLWidget
+except ImportError:
+ app = QApplication(sys.argv)
+ message_box = QMessageBox(QMessageBox.Critical, "OpenGL hellogl",
+ "PyOpenGL must be installed to run this example.",
+ QMessageBox.Close)
+ message_box.setDetailedText("Run:\npip install PyOpenGL PyOpenGL_accelerate")
+ message_box.exec()
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ parser = ArgumentParser(description="hellogl2",
+ formatter_class=RawTextHelpFormatter)
+ parser.add_argument('--multisample', '-m', action='store_true',
+ help='Use Multisampling')
+ parser.add_argument('--coreprofile', '-c', action='store_true',
+ help='Use Core Profile')
+ parser.add_argument('--transparent', '-t', action='store_true',
+ help='Transparent Windows')
+ options = parser.parse_args()
+
+ fmt = QSurfaceFormat()
+ fmt.setDepthBufferSize(24)
+ if options.multisample:
+ fmt.setSamples(4)
+ if options.coreprofile:
+ fmt.setVersion(3, 2)
+ fmt.setProfile(QSurfaceFormat.CoreProfile)
+ QSurfaceFormat.setDefaultFormat(fmt)
+
+ GLWidget.set_transparent(options.transparent)
+
+ main_window = MainWindow()
+ if options.transparent:
+ main_window.setAttribute(Qt.WA_TranslucentBackground)
+ main_window.setAttribute(Qt.WA_NoSystemBackground, False)
+
+ main_window.show()
+
+ res = app.exec()
+ sys.exit(res)
diff --git a/examples/opengl/hellogl2/mainwindow.py b/examples/opengl/hellogl2/mainwindow.py
new file mode 100644
index 000000000..69b9b66fe
--- /dev/null
+++ b/examples/opengl/hellogl2/mainwindow.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import Slot, Qt
+from PySide6.QtGui import QKeySequence
+from PySide6.QtWidgets import QMainWindow, QMessageBox
+
+from window import Window
+
+
+class MainWindow(QMainWindow):
+
+ def __init__(self):
+ super().__init__()
+ menuWindow = self.menuBar().addMenu("Window")
+ menuWindow.addAction("Add new", QKeySequence(Qt.CTRL | Qt.Key_N),
+ self.onAddNew)
+ menuWindow.addAction("Quit", QKeySequence(Qt.CTRL | Qt.Key_Q),
+ qApp.closeAllWindows) # noqa: F821
+
+ self.onAddNew()
+
+ @Slot()
+ def onAddNew(self):
+ if not self.centralWidget():
+ self.setCentralWidget(Window(self))
+ else:
+ QMessageBox.information(self, "Cannot Add Window()",
+ "Already occupied. Undock first.")
diff --git a/examples/opengl/hellogl2/window.py b/examples/opengl/hellogl2/window.py
new file mode 100644
index 000000000..ad61d2f97
--- /dev/null
+++ b/examples/opengl/hellogl2/window.py
@@ -0,0 +1,110 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# Copyright (C) 2013 Riverbank Computing Limited.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+from PySide6.QtCore import Slot, Qt
+from PySide6.QtWidgets import (QHBoxLayout, QMainWindow,
+ QMessageBox, QPushButton, QSlider,
+ QVBoxLayout, QWidget)
+
+from glwidget import GLWidget
+
+
+def _main_window():
+ for t in qApp.topLevelWidgets(): # noqa: F821
+ if isinstance(t, QMainWindow):
+ return t
+ return None
+
+
+class Window(QWidget):
+ instances = [] # Keep references when undocked
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.instances.append(self)
+
+ self._gl_widget = GLWidget()
+
+ self._x_slider = self.create_slider()
+ self._x_slider.valueChanged.connect(self._gl_widget.set_xrotation)
+ self._gl_widget.x_rotation_changed.connect(self._x_slider.setValue)
+
+ self._y_slider = self.create_slider()
+ self._y_slider.valueChanged.connect(self._gl_widget.set_yrotation)
+ self._gl_widget.y_rotation_changed.connect(self._y_slider.setValue)
+
+ self._z_slider = self.create_slider()
+ self._z_slider.valueChanged.connect(self._gl_widget.set_zrotation)
+ self._gl_widget.z_rotation_changed.connect(self._z_slider.setValue)
+
+ mainLayout = QVBoxLayout(self)
+ w = QWidget()
+ container = QHBoxLayout(w)
+ container.addWidget(self._gl_widget)
+ container.addWidget(self._x_slider)
+ container.addWidget(self._y_slider)
+ container.addWidget(self._z_slider)
+
+ mainLayout.addWidget(w)
+ self._dock_btn = QPushButton("Undock")
+ self._dock_btn.clicked.connect(self.dock_undock)
+ mainLayout.addWidget(self._dock_btn)
+
+ self._x_slider.setValue(15 * 16)
+ self._y_slider.setValue(345 * 16)
+ self._z_slider.setValue(0 * 16)
+
+ self.setWindowTitle(self.tr("Hello GL"))
+
+ def create_slider(self):
+ slider = QSlider(Qt.Vertical)
+
+ slider.setRange(0, 360 * 16)
+ slider.setSingleStep(16)
+ slider.setPageStep(15 * 16)
+ slider.setTickInterval(15 * 16)
+ slider.setTickPosition(QSlider.TicksRight)
+ return slider
+
+ def closeEvent(self, event):
+ self.instances.remove(self)
+ event.accept()
+
+ def keyPressEvent(self, event):
+ if self.isWindow() and event.key() == Qt.Key_Escape:
+ self.close()
+ else:
+ super().keyPressEvent(event)
+
+ @Slot()
+ def dock_undock(self):
+ if self.parent():
+ self.undock()
+ else:
+ self.dock()
+
+ def dock(self):
+ mainWindow = _main_window()
+ if not mainWindow or not mainWindow.isVisible():
+ QMessageBox.information(self, "Cannot Dock",
+ "Main window already closed")
+ return
+ if mainWindow.centralWidget():
+ QMessageBox.information(self, "Cannot Dock",
+ "Main window already occupied")
+ return
+
+ self.setAttribute(Qt.WA_DeleteOnClose, False)
+ self._dock_btn.setText("Undock")
+ mainWindow.setCentralWidget(self)
+
+ def undock(self):
+ self.setParent(None)
+ self.setAttribute(Qt.WA_DeleteOnClose)
+ geometry = self.screen().availableGeometry()
+ x = geometry.x() + (geometry.width() - self.width()) / 2
+ y = geometry.y() + (geometry.height() - self.height()) / 2
+ self.move(x, y)
+ self._dock_btn.setText("Dock")
+ self.show()
diff --git a/examples/opengl/textures/textures.py b/examples/opengl/textures/textures.py
index cbefe41d6..87c1164b7 100644
--- a/examples/opengl/textures/textures.py
+++ b/examples/opengl/textures/textures.py
@@ -1,189 +1,170 @@
+# Copyright (C) 2013 Riverbank Computing Limited.
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-############################################################################
-##
-## Copyright (C) 2013 Riverbank Computing Limited.
-## Copyright (C) 2016 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 opengl/textures example from Qt v5.x"""
+"""PySide6 port of the opengl/textures example from Qt v6.x showing the use
+ of legacy OpenGL functions with QOpenGLVersionFunctionsFactory."""
import sys
-from PySide6 import QtCore, QtGui, QtWidgets, QtOpenGL
+from PySide6.QtCore import QPoint, QSize, Qt, QTimer, Signal
+from PySide6.QtGui import QColor, QImage, QSurfaceFormat
+from PySide6.QtWidgets import QApplication, QGridLayout, QMessageBox, QWidget
+from PySide6.QtOpenGL import (QOpenGLTexture, QOpenGLVersionFunctionsFactory,
+ QOpenGLVersionProfile)
+from PySide6.QtOpenGLWidgets import QOpenGLWidget
try:
- from OpenGL.GL import *
+ from OpenGL import GL
except ImportError:
- app = QtWidgets.QApplication(sys.argv)
- messageBox = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Critical, "OpenGL textures",
- "PyOpenGL must be installed to run this example.",
- QtWidgets.QMessageBox.Close)
+ app = QApplication(sys.argv)
+ messageBox = QMessageBox(QMessageBox.Critical, "OpenGL textures",
+ "PyOpenGL must be installed to run this example.",
+ QMessageBox.Close)
messageBox.setDetailedText("Run:\npip install PyOpenGL PyOpenGL_accelerate")
messageBox.exec()
sys.exit(1)
-import textures_rc
+import textures_rc # noqa: F401
-class GLWidget(QtOpenGL.QGLWidget):
+class GLWidget(QOpenGLWidget):
sharedObject = 0
refCount = 0
coords = (
- ( ( +1, -1, -1 ), ( -1, -1, -1 ), ( -1, +1, -1 ), ( +1, +1, -1 ) ),
- ( ( +1, +1, -1 ), ( -1, +1, -1 ), ( -1, +1, +1 ), ( +1, +1, +1 ) ),
- ( ( +1, -1, +1 ), ( +1, -1, -1 ), ( +1, +1, -1 ), ( +1, +1, +1 ) ),
- ( ( -1, -1, -1 ), ( -1, -1, +1 ), ( -1, +1, +1 ), ( -1, +1, -1 ) ),
- ( ( +1, -1, +1 ), ( -1, -1, +1 ), ( -1, -1, -1 ), ( +1, -1, -1 ) ),
- ( ( -1, -1, +1 ), ( +1, -1, +1 ), ( +1, +1, +1 ), ( -1, +1, +1 ) )
+ ((+1, -1, -1), (-1, -1, -1), (-1, +1, -1), (+1, +1, -1)),
+ ((+1, +1, -1), (-1, +1, -1), (-1, +1, +1), (+1, +1, +1)),
+ ((+1, -1, +1), (+1, -1, -1), (+1, +1, -1), (+1, +1, +1)),
+ ((-1, -1, -1), (-1, -1, +1), (-1, +1, +1), (-1, +1, -1)),
+ ((+1, -1, +1), (-1, -1, +1), (-1, -1, -1), (+1, -1, -1)),
+ ((-1, -1, +1), (+1, -1, +1), (+1, +1, +1), (-1, +1, +1))
)
- clicked = QtCore.Signal()
+ clicked = Signal()
- def __init__(self, parent, shareWidget):
- QtOpenGL.QGLWidget.__init__(self, parent, shareWidget)
+ def __init__(self, parent):
+ super().__init__(parent)
- self.clearColor = QtCore.Qt.black
+ self.clearColor = Qt.black
self.xRot = 0
self.yRot = 0
self.zRot = 0
- self.clearColor = QtGui.QColor()
- self.lastPos = QtCore.QPoint()
+ self.clearColor = QColor()
+ self.lastPos = QPoint()
+ self.funcs = None
def freeGLResources(self):
GLWidget.refCount -= 1
if GLWidget.refCount == 0:
self.makeCurrent()
- glDeleteLists(self.__class__.sharedObject, 1)
+ self.funcs.glDeleteLists(self.__class__.sharedObject, 1)
def minimumSizeHint(self):
- return QtCore.QSize(50, 50)
+ return QSize(50, 50)
def sizeHint(self):
- return QtCore.QSize(200, 200)
+ return QSize(200, 200)
def rotateBy(self, xAngle, yAngle, zAngle):
self.xRot = (self.xRot + xAngle) % 5760
self.yRot = (self.yRot + yAngle) % 5760
self.zRot = (self.zRot + zAngle) % 5760
- self.updateGL()
+ self.update()
def setClearColor(self, color):
self.clearColor = color
- self.updateGL()
+ self.update()
def initializeGL(self):
+ profile = QOpenGLVersionProfile()
+ profile.setVersion(3, 2)
+ profile.setProfile(QSurfaceFormat.CompatibilityProfile)
+ self.funcs = QOpenGLVersionFunctionsFactory.get(profile)
+ self.funcs.initializeOpenGLFunctions()
+
if not GLWidget.sharedObject:
self.textures = []
for i in range(6):
- self.textures.append(self.bindTexture(QtGui.QPixmap(f":/images/side{i + 1}.png")))
+ image = QImage(f":/images/side{i + 1}.png")
+ self.textures.append(QOpenGLTexture(image))
GLWidget.sharedObject = self.makeObject()
GLWidget.refCount += 1
- glEnable(GL_DEPTH_TEST)
- glEnable(GL_CULL_FACE)
- glEnable(GL_TEXTURE_2D)
+ self.funcs.glEnable(GL.GL_DEPTH_TEST)
+ self.funcs.glEnable(GL.GL_CULL_FACE)
+ self.funcs.glEnable(GL.GL_TEXTURE_2D)
def paintGL(self):
- self.qglClearColor(self.clearColor)
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
- glLoadIdentity()
- glTranslated(0.0, 0.0, -10.0)
- glRotated(self.xRot / 16.0, 1.0, 0.0, 0.0)
- glRotated(self.yRot / 16.0, 0.0, 1.0, 0.0)
- glRotated(self.zRot / 16.0, 0.0, 0.0, 1.0)
- glCallList(GLWidget.sharedObject)
+ self.funcs.glClearColor(self.clearColor.red(), self.clearColor.green(),
+ self.clearColor.blue(), 1)
+ self.funcs.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
+ self.funcs.glLoadIdentity()
+ self.funcs.glTranslated(0.0, 0.0, -10.0)
+ self.funcs.glRotated(self.xRot / 16.0, 1.0, 0.0, 0.0)
+ self.funcs.glRotated(self.yRot / 16.0, 0.0, 1.0, 0.0)
+ self.funcs.glRotated(self.zRot / 16.0, 0.0, 0.0, 1.0)
+ self.funcs.glCallList(GLWidget.sharedObject)
def resizeGL(self, width, height):
side = min(width, height)
- glViewport(int((width - side) / 2), int((height - side) / 2), side, side)
+ x = int((width - side) / 2)
+ y = int((height - side) / 2)
+ self.funcs.glViewport(x, y, side, side)
- glMatrixMode(GL_PROJECTION)
- glLoadIdentity()
- glOrtho(-0.5, +0.5, +0.5, -0.5, 4.0, 15.0)
- glMatrixMode(GL_MODELVIEW)
+ self.funcs.glMatrixMode(GL.GL_PROJECTION)
+ self.funcs.glLoadIdentity()
+ self.funcs.glOrtho(-0.5, +0.5, +0.5, -0.5, 4.0, 15.0)
+ self.funcs.glMatrixMode(GL.GL_MODELVIEW)
def mousePressEvent(self, event):
- self.lastPos = QtCore.QPoint(event.pos())
+ self.lastPos = event.position().toPoint()
def mouseMoveEvent(self, event):
- dx = event.x() - self.lastPos.x()
- dy = event.y() - self.lastPos.y()
+ pos = event.position().toPoint()
+ dx = pos.x() - self.lastPos.x()
+ dy = pos.y() - self.lastPos.y()
- if event.buttons() & QtCore.Qt.LeftButton:
+ if event.buttons() & Qt.LeftButton:
self.rotateBy(8 * dy, 8 * dx, 0)
- elif event.buttons() & QtCore.Qt.RightButton:
+ elif event.buttons() & Qt.RightButton:
self.rotateBy(8 * dy, 0, 8 * dx)
- self.lastPos = QtCore.QPoint(event.pos())
+ self.lastPos = pos
def mouseReleaseEvent(self, event):
self.clicked.emit()
def makeObject(self):
- dlist = glGenLists(1)
- glNewList(dlist, GL_COMPILE)
+ dlist = self.funcs.glGenLists(1)
+ self.funcs.glNewList(dlist, GL.GL_COMPILE)
for i in range(6):
- glBindTexture(GL_TEXTURE_2D, self.textures[i])
+ self.textures[i].bind()
- glBegin(GL_QUADS)
+ self.funcs.glBegin(GL.GL_QUADS)
for j in range(4):
tx = {False: 0, True: 1}[j == 0 or j == 3]
ty = {False: 0, True: 1}[j == 0 or j == 1]
- glTexCoord2d(tx, ty)
- glVertex3d(0.2 * GLWidget.coords[i][j][0],
- 0.2 * GLWidget.coords[i][j][1],
- 0.2 * GLWidget.coords[i][j][2])
+ self.funcs.glTexCoord2d(tx, ty)
+ x = 0.2 * GLWidget.coords[i][j][0]
+ y = 0.2 * GLWidget.coords[i][j][1]
+ z = 0.2 * GLWidget.coords[i][j][2]
+ self.funcs.glVertex3d(x, y, z)
- glEnd()
+ self.funcs.glEnd()
- glEndList()
+ self.funcs.glEndList()
return dlist
-class Window(QtWidgets.QWidget):
+class Window(QWidget):
NumRows = 2
NumColumns = 3
def __init__(self, parent=None):
- QtWidgets.QWidget.__init__(self, parent)
+ QWidget.__init__(self, parent)
- mainLayout = QtWidgets.QGridLayout()
+ mainLayout = QGridLayout(self)
self.glWidgets = []
for i in range(Window.NumRows):
@@ -191,26 +172,25 @@ class Window(QtWidgets.QWidget):
for j in range(Window.NumColumns):
self.glWidgets[i].append(None)
+ hue_div = (Window.NumRows * Window.NumColumns - 1)
for i in range(Window.NumRows):
for j in range(Window.NumColumns):
- clearColor = QtGui.QColor()
- clearColor.setHsv(((i * Window.NumColumns) + j) * 255
- / (Window.NumRows * Window.NumColumns - 1),
- 255, 63)
-
- self.glWidgets[i][j] = GLWidget(self, self.glWidgets[0][0])
- self.glWidgets[i][j].setClearColor(clearColor)
- self.glWidgets[i][j].rotateBy(+42 * 16, +42 * 16, -21 * 16)
- mainLayout.addWidget(self.glWidgets[i][j], i, j)
+ clearColor = QColor()
+ hue = ((i * Window.NumColumns) + j) * 255 / hue_div
+ clearColor.setHsv(hue, 255, 63)
- self.glWidgets[i][j].clicked.connect(self.setCurrentGlWidget)
- qApp.lastWindowClosed.connect(self.glWidgets[i][j].freeGLResources)
+ glw = GLWidget(self)
+ self.glWidgets[i][j] = glw
+ glw.setClearColor(clearColor)
+ glw.rotateBy(+42 * 16, +42 * 16, -21 * 16)
+ mainLayout.addWidget(glw, i, j)
- self.setLayout(mainLayout)
+ glw.clicked.connect(self.setCurrentGlWidget)
+ qApp.lastWindowClosed.connect(glw.freeGLResources) # noqa: F821
self.currentGlWidget = self.glWidgets[0][0]
- timer = QtCore.QTimer(self)
+ timer = QTimer(self)
timer.timeout.connect(self.rotateOneStep)
timer.start(20)
@@ -225,7 +205,7 @@ class Window(QtWidgets.QWidget):
if __name__ == "__main__":
- app = QtWidgets.QApplication(sys.argv)
+ app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
diff --git a/examples/opengl/textures/textures.pyproject b/examples/opengl/textures/textures.pyproject
index 05416190a..1ad304324 100644
--- a/examples/opengl/textures/textures.pyproject
+++ b/examples/opengl/textures/textures.pyproject
@@ -1,3 +1,3 @@
{
- "files": ["textures.qrc", "textures_rc.py", "textures.py"]
+ "files": ["textures.qrc", "textures.py"]
}
diff --git a/examples/opengl/textures/textures_rc.py b/examples/opengl/textures/textures_rc.py
index e68b63d69..a0676a335 100644
--- a/examples/opengl/textures/textures_rc.py
+++ b/examples/opengl/textures/textures_rc.py
@@ -1,6 +1,6 @@
# Resource object code (Python 3)
# Created by: object code
-# Created by: The Resource Compiler for Qt version 5.14.0
+# Created by: The Resource Compiler for Qt version 6.2.2
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
@@ -285,267 +285,6 @@ F\xd3\xfc\x15\x03\xac\x96\x07k1\xbdh?]\x19'\
\x80\x00\x04 \x00*\x00\xda\x22\x00\x01\x08p\x07\x80\xbb\
\x08@\x00\x02\x10\x00z\xfd\x06\x0eL\xb1gp\xf4v\
\x0b\x00\x00\x00\x00IEND\xaeB`\x82\
-\x00\x00\x09\x13\
-\x89\
-PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
-\x00\x01\x00\x00\x00\x01\x00\x08\x03\x00\x00\x00k\xacXT\
-\x00\x00\x00\xedPLTEx\x00\xf8\x80\x00\xf8\x80\x04\
-\xf8\x80\x08\xf8\x80\x0c\xf8\x88\x10\xf8\x88\x14\xf8\x88\x18\xf8\
-\x88\x1c\xf8\x88 \xf8\x90 \xf8\x90$\xf8\x90(\xf8\x90\
-,\xf8\x900\xf8\x980\xf8\x984\xf8\x988\xf8\x98<\
-\xf8\x98@\xf8\xa0@\xf8\xa0D\xf8\xa0H\xf8\xa0L\xf8\
-\xa0P\xf8\xa8P\xf8\xa8T\xf8\xa8X\xf8\xa8\x5c\xf8\xa8\
-`\xf8\xb0`\xf8\xb0d\xf8\xb0h\xf8\xb0l\xf8\xb0p\
-\xf8\xb8p\xf8\xb8t\xf8\xb8x\xf8\xb8|\xf8\xb8\x80\xf8\
-\xc0\x80\xf8\xc0\x84\xf8\xc0\x88\xf8\xc0\x8c\xf8\xc0\x90\xf8\xc8\
-\x90\xf8\xc8\x94\xf8\xc8\x98\xf8\xc8\x9c\xf8\xc8\xa0\xf8\xd0\xa0\
-\xf8\xd0\xa4\xf8\xd0\xa8\xf8\xd0\xac\xf8\xd0\xb0\xf8\xd8\xb0\xf8\
-\xd8\xb4\xf8\xd8\xb8\xf8\xd8\xbc\xf8\xd8\xc0\xf8\xe0\xc0\xf8\xe0\
-\xc4\xf8\xe0\xc8\xf8\xe0\xcc\xf8\xe0\xd0\xf8\xe8\xd0\xf8\xe8\xd4\
-\xf8\xe8\xd8\xf8\xe8\xdc\xf8\xe8\xe0\xf8\xf0\xe0\xf8\xf0\xe4\xf8\
-\xf0\xe8\xf8\xf0\xec\xf8\xf0\xf0\xf8\xf8\xf0\xf8\xf8\xf4\xf8\xf8\
-\xf8\xf8\xf8\xfc\xf8\x09\xd19\xc7\x00\x00\x00\x09pHY\
-s\x00\x00\x00H\x00\x00\x00H\x00F\xc9k>\x00\x00\
-\x07\xccIDATx\xda\xed\xddiC\xdaL\x10\x00\
-\xe0\x1cP\x90\xa3(-R\x81\xaax\x03\xe5\xa8\x82\x22\
-E\xa8\x81\x0a\x91d\xfe\xff\xcf\xe9\x87\xbe\xafr\xe4\xce\
-&f6\xb3\xdf#\xd9G\xc81;3+@\xcc\x87\
-@\x00\x04@\x00\x04@\x00 \xc4p\x10\x00\x01\x10\xc0\
-&@\xac.}\x04@\x00\x04@\x00\x04@\x00\x04@\
-\x00\x04@\x00\x04@\x00\x04@\x00\x04@\x00\x04@\x00\
-\x04@\x00\x04@\x00\x04@\x00\x04@\x00\x04@\x00\x04\
-@\x00\x04@\x00\x04@\x00\x04@\x00\x04@\x00\xac\x87\
-\xae\x0cZg\x95/\xf9tR\x96\x04)\xf1)\x9d-\
-V\xce[}E\x8f\x03\x80\xd2=\xfe,\x99\xa4,J\
-\x85Zg\xc63\xc0\xea\xae\x96\xb2M\xdcLUo_\
-\xb9\x04\xd0\xee\x0e%\x87\xc9\xab\xd2\xb7\xbe\xc6\x1b\xc0\xe4\
-Xv\x95\xc0\x9b8\x9fs\x04\xa0\xff,\xb8\xcfa\x16\
-+\x13N\x00^\x1b\x9f<\xe6qW\xe7\x1c\x00\xa8W\
-\x09\xef\x99\xec\xd2\xf9\x0a9\xc0\xab\x9f\xe9\x0b\x82 d\
-\xc6\x98\x01\xb4V\xd2w9\x83x\xa6\xa1\x05\xb8K3\
-\xa9\xe88X\xe2\x04P\xf7Y\xd5\xb4\xa4\x9fQ\x02<\
-\xb0\xab\xea\x91\xc7\x18\x01\xae\x18\xd65%\x9e\x10\x02\x1c\
-\xb2\xac\xecJL\xf1\x01$Y\x02\x08\xa9%6\x809\
-\xe3\xea\xbe\xa2\x8e\x0c\xe0\x8eu}\xe3\x052\x80s\xd6\
-\x00\xe2\x04\x17\xc0\x01\xf3\x12\xd7}T\x00\xba\xcc\x1c@\
-\xe8`\x02\xf8\x1d@\x95sJG\x04\xd0\x0d\xa2\xce\xfb\
-\x16\x11\xc0I\x10\x00yD\x00\xf9@J\xfd'h\x00\
-4\xd1\xf4n\x96\xab\x5cw\x87OSE\x99\x8e\x07\x9d\
-\xcb\xf2\x9e\x1b\x80K4\x00c\x93\x17\xdb\xb3\xe1N\x8c\
-k\xd9?q\x1c3\xcc\xa1\x01h\x19\x86yGf\xf7\
-\xcca\xd9\xa1\xc0\x1c\x0b@\xd5 \xc4k\xb9\xe8\xa5T\
-\x1c\x01\x0c\xb0\x00dv\xbe\xbc\xb61\x8dQ\xd6\x01\xc0\
-\x15\x12\x80\xe5\xf6\x89\x9f8\x88l\xaej\xf6\x00e$\
-\x00\xc3\xad_\x7f\xcf\xd9a\x0d[\x80\xcfH\x00n6\
-\x977\x86N\x8fk\xdb.\x13 \x01\xd8\x08\x87\x89\xf7\
-\xce\x0f\xbc\xb0\x01H\x22\x01\xd8\xb8\xb1\xf7\xdc\x1c\xf9\xc5\
-&(\x80\x03\xe0\xcf\xc6\xf5\xcf]$\xcdz)M\xc6\
-\x01\xd0_\x7f\x81\xd1\xfc\x5c>\x90\xfe\x04\xd6~\xc9\xa2\
-\xdbx\xf6\xca2\x9a\x9c\xc6\x01P\xf4\x13\xca\xb4\x0c&\
-\x16q\x00\xbc\x87\xc3\x92\xee\xf3\x9d\x9e-S&P\x00\
-\xacM\xe1\x07\x8b\xc7\xe8\xb5q\x83\x02\xa0\xe7/\x8ag\
-\x15L\x1a\xa2\x008};\xdf\x06\xebp\xe2\x12\x05\xc0\
-[J\x98\xacz9\xfc\x97\xf9\xfc\x0b(\x02\x22\xda[\
->\xe4wO\xc7\xbf\x84\xf76\x1c\x0c\xc0\xd3\xdb\xf9z\
-\xcbkP\xcd\x01~\xa3\x00h\xfb|u\xd3C\x5c\x1c\
-\x0b\x04\xa0\xeas=W3\x05\xe8\xe1\x00\xc8\xfa\x8c\xe2\
-/M\x9f\x83\x91,\x8d\xfd\xec\xfd\x1b?=\x1e?\xe3\
-bq\x94]<\xed=\xae\xaa\xc7\x04\xc0,.\xf6\x0b\
-b\x02pl<\xff\x13\x88\x0b\x80\xf1\xcbPN\x8b\x0b\
-\x80qz\x99\x1cL\xbal\x14\x01\x9a\x86\xc1\xd0\x07\x88\
-\x0d@\xce\x08\xe0\x07\xc4\x06`d4\xff\x06\xc4\x07\xa0\
-\x18B\x1c(\xca\x00\x06I\xf6b\x07\xe2\x03\xf0\xba[\
-d\x22\x0f F\x00\xbb+\xe4{S\x88\x11\xc0nf\
-MI\x85\x18\x01\x0c\xb6s\xcb\xa4V\xc0\x9f\x18-\x80\
-\xbb\xed\xf9\xe7~C\x9c\x00\x1a[\xf3\x97\xae\x83o\xa8\
-\x10!\x80\xe5v\xa6\x5cQ\x09\xe1S\xa3\x03\xd0\xdbZ\
-\x14N\xdf\x85\xf2\xb1Q\x01\x18l\xa5\x16\xcbW\xdc5\
-P\xb0\xfa\xf2\xb7\xb6\xde\x7f\xa4\xfa2\xac\xcf\xfex\x00\
-\xa5U\x92vj\xe6\x07S\x95S\x00u\xf9\xdfX\xcc\
-\x95\xa7A\xab^4\xcd\x08J\xee\xd7\x1a\xc3\x05o\x00\
-\xaa\xe8\xb6l\xba\xda}\xe1\x09`\xe0\xa5H\xa2\xd0\x5c\
-p\x03\xe0\xb1\x8eF\xac<q\x02\xb0'x\x1d\xa5\x09\
-\x0f\x003?\xe5B\xd5\x17\xfc\x00\x1d\x7f\x15\xf4=\xf4\
-\x00e\xc1\xdf8Z\xe1\x06\xd0}v\xd4\x11\x84\xac\x82\
-\x1a`\xec\xbfn0\xf9\x84\x19\x80ES\x11\xf9\x111\
-\x00\x93\xb6:\xf2\x08-\x80\xeb\xe7`\x93\x9b\x81\x82\x15\
-\xa0\xcf\xa8~8\xa3\x22\x05`VO^F\x0a\x90f\
-\x05 tQ\x02\xcc\x98\xcd_H,0\x02l\xa7>\
-\x89\x92w\x81\x1aF\x80\x7f\xa9O\x99\xf2E\xe7a2\
-W5\x00\x00m9\x1b\xf5\xae\xca)\xf7\x02S\x84\x00\
-\xfae\xf1\xfa\xd1\xb8\x80fq[q\xf9\x94\x5c\xe1*\
-*\x0c\x00\xa0\xdf\x1f\xbayL\x10\xe7\xbc\x01\x00\xc0\xfc\
-\xd8\x05\xc1%\x87\x00\x00\xb3#\xe7-\x85\xb8\x04\x00\x18\
-8\xee&2\xe6\x13\x00\x96\xa5p{\xcbE/IJ\
-\xaf;\x03\xf8\xcc+\x80I\xa6\xe8\xeeP\xb9\x05\xb0\xa9\
-\x1f\xff\x7f\x8c\xf8\x05X+\xbc\xb4\x18-\x8e\x01t'\
-\xcd\x18O9\x06\x80\x85\x83'\xe3\x12\xcf\x00N\x96P\
-\xf2\x5c\x038\x08\xa0\xa6\xf8\x06\x18\xd9GE\xf8\x06\xb0\
-\xff\x0aH\x9c\x03\xd8\xe6R\x88\x9c\x03\xe8v\xad\xc9\x93\
-\x9c\x03\xd8F\xd13\xbc\x03<\xda\xa5\x0e\xf1\x0e\xb0\xb2\
-\x89\x0f}\xe5\x1d\xc0\xee>p\xc4=@=\xc6\xef\x02\
-\x00`\xdb\x5c\xb1\xc3=\xc0\xd0\x1a\xe0\x89{\x00\xc5\xfa\
-9H\xe3\x1e`\x11F\x8f\xe9(\x03\xbc\x0a!\xb4\x95\
-\x8b2\x80n\x09\xd0\xe6\x1f@\xb3\x04x\x89,\x80\xfa\
-\xc0\xa8\xd6}i5\x7fV=\xa5\x18\x03\xa8\xf7\xe7\x05\
-\x91U\xd3?\xcb\x9d\x9a\x1aQ\x04\xb8\xcc\x8b,\xff=\
-\x13+\x80Y\x14\x01\xde[\x1f0I\xe7\xec\x87\xb1\xd1\
-\x02S\x80\xb3\xb7\xf3;`qn\x8d\xe0\xef\x01\x8c\x01\
-\xd6v\x17c\xd1\xf3\xe5\xbbE4H\x8b$\x80\xc26\
-h\x9f\x0d\xa3\xb1$\xdb\xbb\xc0\xda\xeeZ\xfe\xb7D\xb2\
-\xb8\x0bJQ\xcd\x13\x5c[\xd2K\xfb\xfe\x92Z\x84\x85\
-\x19v\x15c\x0bp\xca\xf2[j\xbe\xdf\x864\x8f*\
-\xc0zC`\xf9\x8f\xcf\x07\xe1D(\xade\xd9\x02L\
-\x18\xe6t\x9b\xff\x02\xd2Zd\x016\xf7\xd7\xf2\xd7\x01\
-\xe1K8[\x0d1~\x17\xd8\xb8s%\xfd4\x01\x98\
-\x06\x9c\x17\x10\x10\xc0\xe6nQ\x87\xc0\xea/\xad/\x0a\
-\xcf\xa2\x0cp\xbdy\xb2M\xcf\x7f\xc8\xbc\xc2\xae\x0fQ\
-\x06\xd8\xbar\x89\x9e3\xb9\x0a\xc1\xae\x06\x04\x060c\
-T\xe1e\x9a*X\xd0\xa2\x0d\xa0oW\x81\xecy\x8a\
-\x5cM\xcc\x8aI\x12\xacw\xdbc\x1e\x12\xdb\xd9h3\
-\xeb\xe1\xb1]5\xdbcCb\xbd\xbf\x04{\x80\xddm\
-\x06\xb3\xae\x9f\x085\xb3,A1\x80\xdeJ\xac\x01\x0c\
-\x82\x18)\x97\xfdp\xf5r\x88\xad\xc5\x99\x03\x18=\xc0\
-&\xee\xdd\xfc\x85U)\xe88h\xa0\x00\xc6\xc5\x81u\
-\xe7\xd7\xee\xc5~\xc0\x05\x02\x01\x03\x80\xf1\xf5;\xeb\xb4\
-/\xfa\xd0\xb4b\xa4\x098\x00\xcc\xf6[\xae:\xb9\x16\
-\xae\xceLC\x00A\xf5\x96c\x0eP5\x9dB\xdd\xf6\
-\x91\xe0\xd6\xb4\x8221\x02,\x00\x16\xb1l\xa9j\x95\
-\xd3\xa0\xf7r\xe6\x11\x80g@\x03`\x9d\xe0\x99\xbd1\
-y6\x9e^X\xd4\xcf\x1e\x06\xd8\x5c\x8f9\x80m\x8d\
-x\xe6\xe4v\xeb\x85v\xd1\xaf[\xed8,\xfd\x00@\
-\x04\x00Nj\xc2\xe5B\xe5\xac\xd1\xe9v\xdb\x8d\xcb\xea\
-\x81M\xa5`6\xd0\xbe\xca\x01\x00\xe4\x05\xa6\xe3x\x05\
-\xc8\x00\xaa,\xa7\x9f\x1f\x03`\x03h\xb0\x9b~\xa2\x8d\
-\xb1\xaf\xf0\x03\xb3\xf9\xd7\xc2\xe8\xac\xca\x1e`\xceh\xfa\
-GS\x00\x94\x00\xeb+\xa4\x9e\x87X}\x06\xc0\x0a\xe0\
-\xbfa\x96X\x9b\x01\xe0\x05\xf0\xdb/)\xdb\x5c\x00`\
-\x06\x80\xc7o\xde\x9b\x86\xc9\xdf\xc7\x10\xea\x08&Qr\
-q\x93\xf34\xfbro\x05\xc0\x03\x00\x00L/2.\
-[\xa4\xd5\x1f5\x08\x7f\x04\x99*\xab\xb4J\xce\xee\x08\
-b\xae\xd6V\xe0cF\xc0\xb9\xc2\xfa\xb4]\xcd[)\
-\xc8\xd9Js\xb4\x82\x8f\x1b\xa1$K\xcf\x87\xdd\xab\xe3\
-\xc3b>\x9dL\xc8\x92 %R\x99\xfc\xfe\xd7\xf2i\
-\xb3?Y\xc0G\x8f(g\x8b\x13\x00\x01\x10\x00\x01\x10\
-\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\
-\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\
-\x10\x00\x01\x10\x00\x01\x10@\x80\x00q\x1b\x04@\x00\x04\
-\xf0\x0e\x10\xdfA\x00\x04@\x00\x04\x10\xeb\xf1\x17\xe9\x89\
-Gh\xda\x1b|\x00\x00\x00\x00\x00IEND\xaeB\
-`\x82\
-\x00\x00\x06\xe8\
-\x89\
-PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
-\x00\x01\x00\x00\x00\x01\x00\x08\x03\x00\x00\x00k\xacXT\
-\x00\x00\x00\x8aPLTE\x00\xfcx\x00\xfc\x80\x08\xfc\
-\x80\x10\xfc\x80\x10\xfc\x88\x18\xfc\x88 \xfc\x88 \xfc\x90\
-(\xfc\x900\xfc\x900\xfc\x988\xfc\x98@\xfc\x98@\
-\xfc\xa0H\xfc\xa0P\xfc\xa0P\xfc\xa8X\xfc\xa8`\xfc\
-\xa8`\xfc\xb0h\xfc\xb0p\xfc\xb0p\xfc\xb8x\xfc\xb8\
-\x80\xfc\xb8\x80\xfc\xc0\x88\xfc\xc0\x90\xfc\xc0\x90\xfc\xc8\x98\
-\xfc\xc8\xa0\xfc\xd0\xa8\xfc\xd0\xb0\xfc\xd0\xb0\xfc\xd8\xb8\xfc\
-\xd8\xc0\xfc\xe0\xc8\xfc\xe0\xd0\xfc\xe0\xd0\xfc\xe8\xd8\xfc\xe8\
-\xe0\xfc\xe8\xe0\xfc\xf0\xe8\xfc\xf0\xf0\xfc\xf0\xf0\xfc\xf8\xf8\
-\xfc\xf8`;^\x10\x00\x00\x00\x09pHYs\x00\x00\
-\x00H\x00\x00\x00H\x00F\xc9k>\x00\x00\x06\x04I\
-DATx\xda\xed\xddaw\xa28\x14\x06`Ba\
-`\xa4\xba\xd0vtuQ*2Pb\xf8\xff\x7fo\
-;\x9d=g\xdb\x01\x14\x85$7\xe4\xcdw\xcf\xe9}\
-*\xe1\xe6\xe6&:\x8d\xe5\xc3\x01\x00\x00\x00\x00\x00\x00\
-4\x8e\x85\x03\x00\x00\x00\xc0W\x00\xab\xa6>\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00&\x1eU\x9e\xae\x93(\xf4]\x971\xd7\xf3\x830\
-J\xd6iV\x08\x1b\x00\xc4q\xf3\xe8\xf6u,\x06\xf1\
-\xf6\xc8\xe7\x0cP\xed\x1e\xd9\xd5\xbe\xcdp\x9d\x8bY\x02\
-\xf0\x7f\x16C{W\xd9\xea \xe6\x06P$\xec\xa6\xfe\
-]7\xc9\xe7\x04p\x8c\xee\xe8a\x0eR1\x13\x80\xe3\
-\xe2\xce6no\xcbg\x00PD#:\xd9\xdd\x9d\xe9\
-\x00u2\xb2\x99?8\x1a\x0d\x90\xba\xe3\xcf3,k\
-c\x01\xca\xc5$':\x1e2C\x016l\xaaC-\
-O\xc2@\x80\x89\xfe\xfd\xff\xcd\x04\x95q\x00S<\xfd\
-\x9f\xdf\x88\x85Y\x00<\x9e\xfah\x97{4\x0a\xe0\xdb\
-\xf4\x87\xdb\xd8\xc1 \x00.\xe3x\x1f\xcb\xcd\x01(\xa4\
-\x1cptO\xc6\x00\x1c\xe5\x1c\xf1\xf4*S\x00RI\
-\x87\x5c\xbf\x0bC\x006\xb2\x8e\xf9\xbe\x18\x02\x90\xc8\x02\
-p23\x00\x1e\xa5\x01x\xdc\x08\x80@\xdeY\xf7\x17\
-#\x00\x98<\x00V\x1a\x00\xc0\x1d\x89ci\x00\xc0\x80\
-<\x88\xb9w\x7fK\x0a\xfa\x00\x97\xf2 o\xb5\xcd\x8a\
-\x8f\x99L\xf0\x22\xdb\xae\xbc\x9b\x01\x12\xfa\x00\xbdy\xd0\
-b\xdb~\x82\xcbMx\xe3,\xc0\xc9\x03t\xe7A\xee\
-\xba/\x93=\xc57=\x0f\x7f\x93\x07\xe8\xca\x83\xbc\xdd\
-\xa54\xb6\x5c\xdeR\x1e\x22\x0f\xd0\xde\x09`?\xceW\
->\x93\xdd0\x19T\xd4\x01ZyP8`\xe6\xae\x87\
-\x7f\x09v\xd4\x01\xfe|\xa2\xe3a\x8b\xb8\xe7\xa1\x00\x8f\
-\xc4\x01\xfe\xcc\x83\xd6C?\xb8\x1e\xfa\x1e \x0eP\xdc\
-\x19\xff\xf0U\xe4O\xda\x00\xaf_\xfe\xd8\xbf\xc6M\x9f\
-\x9d\xe3@\x1b\xe0K\x1e\xe4\xdf\x94\xb6\xd4\xc3v\x13\xd6\
-\xb4\x01\xbe<\xca\xfb\xdb>{\x18\x04\xb0\xa2\x0d\xf0\xf9\
-I\xf6\xc7\xe7\x10])5m\x80hL\xfd\xe24\x04\
- \xa4\x0d\xf09\x0f\xba}3cH>\xe4\xd3\x06\xf8\
-\x94\x07\xb1\xdb\xeb\xd8C\xf6\x14\x5c\xd2\x00|d\xf9\xc6\
-W\x99\x09\xc9\x00(Ff\xed/\xa6\x7f\x03^G\xa6\
-l\xf9\x80\xe28i\x80t\xec\x1f\xca\x14V\x04d\x00\
-\xac\xc7\x96\xef\xae\xa7\x02\x11i\x80dl\xce~}Q\
-\x98\x90\x06\xf8\xf4\x0f\xbc\xaf\xc3oo\xf8Z \x18\x9b\
-\xb0\xe5\x86\xaf\x06\xd9\xd8\x7fT\xa9poD\x02\x00\x1f\
-\x93\x07\xff^\x13\x9b]\x11*\xc6\xe4\xc1\x1fC\xa8{\
-\x09\xc8\x00\xc8\xc6oc^\x03\xd8\x90\x06H\xc7W\xaf\
-\xaf\x01\x1ci\x03\x04\xee\xc8\xa9\xea|m% H\x03\
-|Lc?\xf3C\xba\xbd\xfb\xd3\xca\x0abD\x8f\xce\
-VW\x00\xf6s\x078\x19\xbf=>rd\x97\x01\xe2\
-f\xee\x00;e\xef\x00\xa2\x00/\x8a\x0a\xa2d\x01.\
-\xd7\x03\xb6\xf3\x07\xb8\xd8*\xe1\xf2\xd9\x03\xd43\xe8\x14\
-\x95\xf7\x12`o\xf3\x07\xb88\x07>7\xf3\x07\xb8t\
-\xde\x90U\xf3\x07\xe0l\x06\x07&\xc6\x8c\xbd\xbaW\x00\
-M\x80XM\x7f\x1cY\x00\xe1\xaah\x0b \x0cp\xa9\
-G\xe6d\x03\xc0JQ\x9f<U\x80\x0b\xef\x00\x9f\xdb\
-\x00\xb0U\xb4\x0c&\x0b\xd0\xdf\x1e\xf2\xd4\xd8\x00\xd0\xbf\
-\x0e\x08\xceV\x00\xf4\x96\x02\x98a7H\xdc9r\x85\
-)\x10I\x80H\xc5V\x00a\x80\xdezx(\xec\x00\
-\xe8\xfb\x02\xb8Uc\x05@\xdf+\x80I\xbcN\x8c\x14\
-@\xdf\x99\xf3\xb4\xb1\x03`'\xbd#\x8a6@\xdfa\
-\x91\xb8\xb1\x04\xa0g\x19\xb8\x14\x96\x00\xf4\xcc\x80\x0b\xc9\
-\xf7\x8a\x92\x01\xe0\xdd\xab\xa0P\xf6\xa5\xa2d\x00\x12=\
-\xf1\x93\x01\xc84\xc5O\x05\xa0\xf64\xc5O\x05\xa0\xf3\
-\xde\xa1\x85\x8a{\xd6i\x00t\xde8\x11\x9d\x1b[\x00\
-r\xa6\xfe\xfdO\x09\xa0s\x02X)\xba^\x9e\x00\x80\
-X(\xcf\x7fi\x01$\x8a\xb6@\xa8\x02\xecTl\x82\
-\x13\x06\xc8\x98\xe2\xf5/1\x80\xc2\x95z\x1a\x80<@\
-\xe5\xc9\xee\x03\xa4\x0d\xc0\x03\xb5\xf5/j\x00\xe7\xf6\x0b\
-\x90\xed\x1b{\x00D\xa4\xe8\xfal\xaa\x00\xed\x1a\x18\xcb\
-\x1a\x8b\x00bU\x17\xc8\x13\x05\xe8\x88?o,\x02\x88\
-\x95\x5c\x9dN\x17 V\xf6\x13\x124\x01\xda\xf3\x9fW\
-6\xf6\x00\x88\xf6UA~\xd5\xd8\x03\xd0\x11\x7f\xa0+\
-~\x1d\x00\xe7v\xfe\x13\xd6\x8d=\x00|\xa1\xa5\xfcM\
-\x06\x80\x87z\xca\xdfT\x00\xeav\xfc\x91h\xec\x01\xa8\
-\x03]\xe5o\x1a\x00\xd57m\xe5o\x12\x00\x95\xaf\xaf\
-\xfcM\x01\xa0\xf45\x96\xbf\x09\x00\x14\x9e\xce\xf2\xb7~\
-\x80\xe2Ak\xf9[;\xc0\xc9\xd5[\xfe\xd6\x0d\x90\xbb\
-\xca\xfa\xbfI\x02\x1c]\xdd\xe5o\xbd\x00\xafL{\xf9\
-[+@{\xffO}\xf9['\xc0\x81\x11(\x7fk\
-\x04\xd83\x0a\xe5o}\x00)\x8d\xf2\xb76\x80\x1d\x91\
-\xf2\xb7.\x80-\x95\xf2\xb7&\x80v\x03\xa0?E\xf9\
-[pC\x00~\x8c:\xfe$\xf8[Y\x9c\xf2\xec\xb0\
-O\xb7\x9b\xf5K\x12\xaf\x96Q\x18\xf8\x0f\xec\xfe\xdbJ\
-\x15\x03t]\x07S\xb7\xc3\xac\xab\xf70_\xdf\xc3\xdc\
-\xbd\x87\xf9\xfc+\xcc\xef\x81\xef\xb9j\xae\x93\x92\x09\xf0\
-\xd4y\x0bJ\xba\xdb\xbe\x87\x99\xbc\x87\xb9\xb8\x1af\xff\
-H\x0d\x00\x90\xf7\xcb\xb3S\xde\xac,\x0f \x96\x19?\
-\xfd9@\xac\xa4\xc6?\xddm\x22\x92\x00\xc4Rn\xfc\
-NI\x1b@D\x92\xe3\x9f\xee\x17\x07\xe5\xfc\xda\xdcB\
-v\xfc\x0e\xa7\x0c\xa0 ~\xe7L\x18\x80\x87\xf2\xe3w\
-\x04]\x80:P\x10\xfft\xd9\xfa\xe4\x00oJ\xe2\xa7\
-\x0bP\xf9\x8e\xd5\x00\xa5\xa2\xf8\xa9\x02\x94\x9ec5@\
-\xa1,~\xa2\x00+\x07\x00\x00\x00\x00\x00\x00\xa0b\x08\
-\xdb\x018\x00,\x07\xa8m\x07\xa8l\x07(l\x07\xa0\
-^\x167g\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`+\x80\
-m\x03\x00\x00\x00\xc0\xff\x00\xf6\x0e\x00\x00\x00\x00\x00\xb0\
-z\xfc\x0bC\xd4\xc6\xc6D\x07\xe4\xaa\x00\x00\x00\x00I\
-END\xaeB`\x82\
\x00\x00\x09\x8e\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -701,6 +440,267 @@ Q\xee\x10@\x8f\x84\x00\x08\x80\x00\x08\x80\x00\x08\x80\x00\
\x00\x08`*\x80i\x09\x01\x10\x00\x01~\x01\x98\x9b\x10\
\x00\x01\x10\x00\x01\x8cN\xff\x00\xf3k\xd4\xa5uQ\x85\
3\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x06\xe8\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x01\x00\x00\x00\x01\x00\x08\x03\x00\x00\x00k\xacXT\
+\x00\x00\x00\x8aPLTE\x00\xfcx\x00\xfc\x80\x08\xfc\
+\x80\x10\xfc\x80\x10\xfc\x88\x18\xfc\x88 \xfc\x88 \xfc\x90\
+(\xfc\x900\xfc\x900\xfc\x988\xfc\x98@\xfc\x98@\
+\xfc\xa0H\xfc\xa0P\xfc\xa0P\xfc\xa8X\xfc\xa8`\xfc\
+\xa8`\xfc\xb0h\xfc\xb0p\xfc\xb0p\xfc\xb8x\xfc\xb8\
+\x80\xfc\xb8\x80\xfc\xc0\x88\xfc\xc0\x90\xfc\xc0\x90\xfc\xc8\x98\
+\xfc\xc8\xa0\xfc\xd0\xa8\xfc\xd0\xb0\xfc\xd0\xb0\xfc\xd8\xb8\xfc\
+\xd8\xc0\xfc\xe0\xc8\xfc\xe0\xd0\xfc\xe0\xd0\xfc\xe8\xd8\xfc\xe8\
+\xe0\xfc\xe8\xe0\xfc\xf0\xe8\xfc\xf0\xf0\xfc\xf0\xf0\xfc\xf8\xf8\
+\xfc\xf8`;^\x10\x00\x00\x00\x09pHYs\x00\x00\
+\x00H\x00\x00\x00H\x00F\xc9k>\x00\x00\x06\x04I\
+DATx\xda\xed\xddaw\xa28\x14\x06`Ba\
+`\xa4\xba\xd0vtuQ*2Pb\xf8\xff\x7fo\
+;\x9d=g\xdb\x01\x14\x85$7\xe4\xcdw\xcf\xe9}\
+*\xe1\xe6\xe6&:\x8d\xe5\xc3\x01\x00\x00\x00\x00\x00\x00\
+4\x8e\x85\x03\x00\x00\x00\xc0W\x00\xab\xa6>\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00&\x1eU\x9e\xae\x93(\xf4]\x971\xd7\xf3\x830\
+J\xd6iV\x08\x1b\x00\xc4q\xf3\xe8\xf6u,\x06\xf1\
+\xf6\xc8\xe7\x0cP\xed\x1e\xd9\xd5\xbe\xcdp\x9d\x8bY\x02\
+\xf0\x7f\x16C{W\xd9\xea \xe6\x06P$\xec\xa6\xfe\
+]7\xc9\xe7\x04p\x8c\xee\xe8a\x0eR1\x13\x80\xe3\
+\xe2\xce6no\xcbg\x00PD#:\xd9\xdd\x9d\xe9\
+\x00u2\xb2\x99?8\x1a\x0d\x90\xba\xe3\xcf3,k\
+c\x01\xca\xc5$':\x1e2C\x016l\xaaC-\
+O\xc2@\x80\x89\xfe\xfd\xff\xcd\x04\x95q\x00S<\xfd\
+\x9f\xdf\x88\x85Y\x00<\x9e\xfah\x97{4\x0a\xe0\xdb\
+\xf4\x87\xdb\xd8\xc1 \x00.\xe3x\x1f\xcb\xcd\x01(\xa4\
+\x1cptO\xc6\x00\x1c\xe5\x1c\xf1\xf4*S\x00RI\
+\x87\x5c\xbf\x0bC\x006\xb2\x8e\xf9\xbe\x18\x02\x90\xc8\x02\
+p23\x00\x1e\xa5\x01x\xdc\x08\x80@\xdeY\xf7\x17\
+#\x00\x98<\x00V\x1a\x00\xc0\x1d\x89ci\x00\xc0\x80\
+<\x88\xb9w\x7fK\x0a\xfa\x00\x97\xf2 o\xb5\xcd\x8a\
+\x8f\x99L\xf0\x22\xdb\xae\xbc\x9b\x01\x12\xfa\x00\xbdy\xd0\
+b\xdb~\x82\xcbMx\xe3,\xc0\xc9\x03t\xe7A\xee\
+\xba/\x93=\xc57=\x0f\x7f\x93\x07\xe8\xca\x83\xbc\xdd\
+\xa54\xb6\x5c\xdeR\x1e\x22\x0f\xd0\xde\x09`?\xceW\
+>\x93\xdd0\x19T\xd4\x01ZyP8`\xe6\xae\x87\
+\x7f\x09v\xd4\x01\xfe|\xa2\xe3a\x8b\xb8\xe7\xa1\x00\x8f\
+\xc4\x01\xfe\xcc\x83\xd6C?\xb8\x1e\xfa\x1e \x0eP\xdc\
+\x19\xff\xf0U\xe4O\xda\x00\xaf_\xfe\xd8\xbf\xc6M\x9f\
+\x9d\xe3@\x1b\xe0K\x1e\xe4\xdf\x94\xb6\xd4\xc3v\x13\xd6\
+\xb4\x01\xbe<\xca\xfb\xdb>{\x18\x04\xb0\xa2\x0d\xf0\xf9\
+I\xf6\xc7\xe7\x10])5m\x80hL\xfd\xe24\x04\
+ \xa4\x0d\xf09\x0f\xba}3cH>\xe4\xd3\x06\xf8\
+\x94\x07\xb1\xdb\xeb\xd8C\xf6\x14\x5c\xd2\x00|d\xf9\xc6\
+W\x99\x09\xc9\x00(Ff\xed/\xa6\x7f\x03^G\xa6\
+l\xf9\x80\xe28i\x80t\xec\x1f\xca\x14V\x04d\x00\
+\xac\xc7\x96\xef\xae\xa7\x02\x11i\x80dl\xce~}Q\
+\x98\x90\x06\xf8\xf4\x0f\xbc\xaf\xc3oo\xf8Z \x18\x9b\
+\xb0\xe5\x86\xaf\x06\xd9\xd8\x7fT\xa9poD\x02\x00\x1f\
+\x93\x07\xff^\x13\x9b]\x11*\xc6\xe4\xc1\x1fC\xa8{\
+\x09\xc8\x00\xc8\xc6oc^\x03\xd8\x90\x06H\xc7W\xaf\
+\xaf\x01\x1ci\x03\x04\xee\xc8\xa9\xea|m% H\x03\
+|Lc?\xf3C\xba\xbd\xfb\xd3\xca\x0abD\x8f\xce\
+VW\x00\xf6s\x078\x19\xbf=>rd\x97\x01\xe2\
+f\xee\x00;e\xef\x00\xa2\x00/\x8a\x0a\xa2d\x01.\
+\xd7\x03\xb6\xf3\x07\xb8\xd8*\xe1\xf2\xd9\x03\xd43\xe8\x14\
+\x95\xf7\x12`o\xf3\x07\xb88\x07>7\xf3\x07\xb8t\
+\xde\x90U\xf3\x07\xe0l\x06\x07&\xc6\x8c\xbd\xbaW\x00\
+M\x80XM\x7f\x1cY\x00\xe1\xaah\x0b \x0cp\xa9\
+G\xe6d\x03\xc0JQ\x9f<U\x80\x0b\xef\x00\x9f\xdb\
+\x00\xb0U\xb4\x0c&\x0b\xd0\xdf\x1e\xf2\xd4\xd8\x00\xd0\xbf\
+\x0e\x08\xceV\x00\xf4\x96\x02\x98a7H\xdc9r\x85\
+)\x10I\x80H\xc5V\x00a\x80\xdezx(\xec\x00\
+\xe8\xfb\x02\xb8Uc\x05@\xdf+\x80I\xbcN\x8c\x14\
+@\xdf\x99\xf3\xb4\xb1\x03`'\xbd#\x8a6@\xdfa\
+\x91\xb8\xb1\x04\xa0g\x19\xb8\x14\x96\x00\xf4\xcc\x80\x0b\xc9\
+\xf7\x8a\x92\x01\xe0\xdd\xab\xa0P\xf6\xa5\xa2d\x00\x12=\
+\xf1\x93\x01\xc84\xc5O\x05\xa0\xf64\xc5O\x05\xa0\xf3\
+\xde\xa1\x85\x8a{\xd6i\x00t\xde8\x11\x9d\x1b[\x00\
+r\xa6\xfe\xfdO\x09\xa0s\x02X)\xba^\x9e\x00\x80\
+X(\xcf\x7fi\x01$\x8a\xb6@\xa8\x02\xecTl\x82\
+\x13\x06\xc8\x98\xe2\xf5/1\x80\xc2\x95z\x1a\x80<@\
+\xe5\xc9\xee\x03\xa4\x0d\xc0\x03\xb5\xf5/j\x00\xe7\xf6\x0b\
+\x90\xed\x1b{\x00D\xa4\xe8\xfal\xaa\x00\xed\x1a\x18\xcb\
+\x1a\x8b\x00bU\x17\xc8\x13\x05\xe8\x88?o,\x02\x88\
+\x95\x5c\x9dN\x17 V\xf6\x13\x124\x01\xda\xf3\x9fW\
+6\xf6\x00\x88\xf6UA~\xd5\xd8\x03\xd0\x11\x7f\xa0+\
+~\x1d\x00\xe7v\xfe\x13\xd6\x8d=\x00|\xa1\xa5\xfcM\
+\x06\x80\x87z\xca\xdfT\x00\xeav\xfc\x91h\xec\x01\xa8\
+\x03]\xe5o\x1a\x00\xd57m\xe5o\x12\x00\x95\xaf\xaf\
+\xfcM\x01\xa0\xf45\x96\xbf\x09\x00\x14\x9e\xce\xf2\xb7~\
+\x80\xe2Ak\xf9[;\xc0\xc9\xd5[\xfe\xd6\x0d\x90\xbb\
+\xca\xfa\xbfI\x02\x1c]\xdd\xe5o\xbd\x00\xafL{\xf9\
+[+@{\xffO}\xf9['\xc0\x81\x11(\x7fk\
+\x04\xd83\x0a\xe5o}\x00)\x8d\xf2\xb76\x80\x1d\x91\
+\xf2\xb7.\x80-\x95\xf2\xb7&\x80v\x03\xa0?E\xf9\
+[pC\x00~\x8c:\xfe$\xf8[Y\x9c\xf2\xec\xb0\
+O\xb7\x9b\xf5K\x12\xaf\x96Q\x18\xf8\x0f\xec\xfe\xdbJ\
+\x15\x03t]\x07S\xb7\xc3\xac\xab\xf70_\xdf\xc3\xdc\
+\xbd\x87\xf9\xfc+\xcc\xef\x81\xef\xb9j\xae\x93\x92\x09\xf0\
+\xd4y\x0bJ\xba\xdb\xbe\x87\x99\xbc\x87\xb9\xb8\x1af\xff\
+H\x0d\x00\x90\xf7\xcb\xb3S\xde\xac,\x0f \x96\x19?\
+\xfd9@\xac\xa4\xc6?\xddm\x22\x92\x00\xc4Rn\xfc\
+NI\x1b@D\x92\xe3\x9f\xee\x17\x07\xe5\xfc\xda\xdcB\
+v\xfc\x0e\xa7\x0c\xa0 ~\xe7L\x18\x80\x87\xf2\xe3w\
+\x04]\x80:P\x10\xfft\xd9\xfa\xe4\x00oJ\xe2\xa7\
+\x0bP\xf9\x8e\xd5\x00\xa5\xa2\xf8\xa9\x02\x94\x9ec5@\
+\xa1,~\xa2\x00+\x07\x00\x00\x00\x00\x00\x00\xa0b\x08\
+\xdb\x018\x00,\x07\xa8m\x07\xa8l\x07(l\x07\xa0\
+^\x167g\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`+\x80\
+m\x03\x00\x00\x00\xc0\xff\x00\xf6\x0e\x00\x00\x00\x00\x00\xb0\
+z\xfc\x0bC\xd4\xc6\xc6D\x07\xe4\xaa\x00\x00\x00\x00I\
+END\xaeB`\x82\
+\x00\x00\x09\x13\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x01\x00\x00\x00\x01\x00\x08\x03\x00\x00\x00k\xacXT\
+\x00\x00\x00\xedPLTEx\x00\xf8\x80\x00\xf8\x80\x04\
+\xf8\x80\x08\xf8\x80\x0c\xf8\x88\x10\xf8\x88\x14\xf8\x88\x18\xf8\
+\x88\x1c\xf8\x88 \xf8\x90 \xf8\x90$\xf8\x90(\xf8\x90\
+,\xf8\x900\xf8\x980\xf8\x984\xf8\x988\xf8\x98<\
+\xf8\x98@\xf8\xa0@\xf8\xa0D\xf8\xa0H\xf8\xa0L\xf8\
+\xa0P\xf8\xa8P\xf8\xa8T\xf8\xa8X\xf8\xa8\x5c\xf8\xa8\
+`\xf8\xb0`\xf8\xb0d\xf8\xb0h\xf8\xb0l\xf8\xb0p\
+\xf8\xb8p\xf8\xb8t\xf8\xb8x\xf8\xb8|\xf8\xb8\x80\xf8\
+\xc0\x80\xf8\xc0\x84\xf8\xc0\x88\xf8\xc0\x8c\xf8\xc0\x90\xf8\xc8\
+\x90\xf8\xc8\x94\xf8\xc8\x98\xf8\xc8\x9c\xf8\xc8\xa0\xf8\xd0\xa0\
+\xf8\xd0\xa4\xf8\xd0\xa8\xf8\xd0\xac\xf8\xd0\xb0\xf8\xd8\xb0\xf8\
+\xd8\xb4\xf8\xd8\xb8\xf8\xd8\xbc\xf8\xd8\xc0\xf8\xe0\xc0\xf8\xe0\
+\xc4\xf8\xe0\xc8\xf8\xe0\xcc\xf8\xe0\xd0\xf8\xe8\xd0\xf8\xe8\xd4\
+\xf8\xe8\xd8\xf8\xe8\xdc\xf8\xe8\xe0\xf8\xf0\xe0\xf8\xf0\xe4\xf8\
+\xf0\xe8\xf8\xf0\xec\xf8\xf0\xf0\xf8\xf8\xf0\xf8\xf8\xf4\xf8\xf8\
+\xf8\xf8\xf8\xfc\xf8\x09\xd19\xc7\x00\x00\x00\x09pHY\
+s\x00\x00\x00H\x00\x00\x00H\x00F\xc9k>\x00\x00\
+\x07\xccIDATx\xda\xed\xddiC\xdaL\x10\x00\
+\xe0\x1cP\x90\xa3(-R\x81\xaax\x03\xe5\xa8\x82\x22\
+E\xa8\x81\x0a\x91d\xfe\xff\xcf\xe9\x87\xbe\xafr\xe4\xce\
+&f6\xb3\xdf#\xd9G\xc81;3+@\xcc\x87\
+@\x00\x04@\x00\x04@\x00 \xc4p\x10\x00\x01\x10\xc0\
+&@\xac.}\x04@\x00\x04@\x00\x04@\x00\x04@\
+\x00\x04@\x00\x04@\x00\x04@\x00\x04@\x00\x04@\x00\
+\x04@\x00\x04@\x00\x04@\x00\x04@\x00\x04@\x00\x04\
+@\x00\x04@\x00\x04@\x00\x04@\x00\x04@\x00\xac\x87\
+\xae\x0cZg\x95/\xf9tR\x96\x04)\xf1)\x9d-\
+V\xce[}E\x8f\x03\x80\xd2=\xfe,\x99\xa4,J\
+\x85Zg\xc63\xc0\xea\xae\x96\xb2M\xdcLUo_\
+\xb9\x04\xd0\xee\x0e%\x87\xc9\xab\xd2\xb7\xbe\xc6\x1b\xc0\xe4\
+Xv\x95\xc0\x9b8\x9fs\x04\xa0\xff,\xb8\xcfa\x16\
++\x13N\x00^\x1b\x9f<\xe6qW\xe7\x1c\x00\xa8W\
+\x09\xef\x99\xec\xd2\xf9\x0a9\xc0\xab\x9f\xe9\x0b\x82 d\
+\xc6\x98\x01\xb4V\xd2w9\x83x\xa6\xa1\x05\xb8K3\
+\xa9\xe88X\xe2\x04P\xf7Y\xd5\xb4\xa4\x9fQ\x02<\
+\xb0\xab\xea\x91\xc7\x18\x01\xae\x18\xd65%\x9e\x10\x02\x1c\
+\xb2\xac\xecJL\xf1\x01$Y\x02\x08\xa9%6\x809\
+\xe3\xea\xbe\xa2\x8e\x0c\xe0\x8eu}\xe3\x052\x80s\xd6\
+\x00\xe2\x04\x17\xc0\x01\xf3\x12\xd7}T\x00\xba\xcc\x1c@\
+\xe8`\x02\xf8\x1d@\x95sJG\x04\xd0\x0d\xa2\xce\xfb\
+\x16\x11\xc0I\x10\x00yD\x00\xf9@J\xfd'h\x00\
+4\xd1\xf4n\x96\xab\x5cw\x87OSE\x99\x8e\x07\x9d\
+\xcb\xf2\x9e\x1b\x80K4\x00c\x93\x17\xdb\xb3\xe1N\x8c\
+k\xd9?q\x1c3\xcc\xa1\x01h\x19\x86yGf\xf7\
+\xcca\xd9\xa1\xc0\x1c\x0b@\xd5 \xc4k\xb9\xe8\xa5T\
+\x1c\x01\x0c\xb0\x00dv\xbe\xbc\xb61\x8dQ\xd6\x01\xc0\
+\x15\x12\x80\xe5\xf6\x89\x9f8\x88l\xaej\xf6\x00e$\
+\x00\xc3\xad_\x7f\xcf\xd9a\x0d[\x80\xcfH\x00n6\
+\x977\x86N\x8fk\xdb.\x13 \x01\xd8\x08\x87\x89\xf7\
+\xce\x0f\xbc\xb0\x01H\x22\x01\xd8\xb8\xb1\xf7\xdc\x1c\xf9\xc5\
+&(\x80\x03\xe0\xcf\xc6\xf5\xcf]$\xcdz)M\xc6\
+\x01\xd0_\x7f\x81\xd1\xfc\x5c>\x90\xfe\x04\xd6~\xc9\xa2\
+\xdbx\xf6\xca2\x9a\x9c\xc6\x01P\xf4\x13\xca\xb4\x0c&\
+\x16q\x00\xbc\x87\xc3\x92\xee\xf3\x9d\x9e-S&P\x00\
+\xacM\xe1\x07\x8b\xc7\xe8\xb5q\x83\x02\xa0\xe7/\x8ag\
+\x15L\x1a\xa2\x008};\xdf\x06\xebp\xe2\x12\x05\xc0\
+[J\x98\xacz9\xfc\x97\xf9\xfc\x0b(\x02\x22\xda[\
+>\xe4wO\xc7\xbf\x84\xf76\x1c\x0c\xc0\xd3\xdb\xf9z\
+\xcbkP\xcd\x01~\xa3\x00h\xfb|u\xd3C\x5c\x1c\
+\x0b\x04\xa0\xeas=W3\x05\xe8\xe1\x00\xc8\xfa\x8c\xe2\
+/M\x9f\x83\x91,\x8d\xfd\xec\xfd\x1b?=\x1e?\xe3\
+bq\x94]<\xed=\xae\xaa\xc7\x04\xc0,.\xf6\x0b\
+b\x02pl<\xff\x13\x88\x0b\x80\xf1\xcbPN\x8b\x0b\
+\x80qz\x99\x1cL\xbal\x14\x01\x9a\x86\xc1\xd0\x07\x88\
+\x0d@\xce\x08\xe0\x07\xc4\x06`d4\xff\x06\xc4\x07\xa0\
+\x18B\x1c(\xca\x00\x06I\xf6b\x07\xe2\x03\xf0\xba[\
+d\x22\x0f F\x00\xbb+\xe4{S\x88\x11\xc0nf\
+MI\x85\x18\x01\x0c\xb6s\xcb\xa4V\xc0\x9f\x18-\x80\
+\xbb\xed\xf9\xe7~C\x9c\x00\x1a[\xf3\x97\xae\x83o\xa8\
+\x10!\x80\xe5v\xa6\x5cQ\x09\xe1S\xa3\x03\xd0\xdbZ\
+\x14N\xdf\x85\xf2\xb1Q\x01\x18l\xa5\x16\xcbW\xdc5\
+P\xb0\xfa\xf2\xb7\xb6\xde\x7f\xa4\xfa2\xac\xcf\xfex\x00\
+\xa5U\x92vj\xe6\x07S\x95S\x00u\xf9\xdfX\xcc\
+\x95\xa7A\xab^4\xcd\x08J\xee\xd7\x1a\xc3\x05o\x00\
+\xaa\xe8\xb6l\xba\xda}\xe1\x09`\xe0\xa5H\xa2\xd0\x5c\
+p\x03\xe0\xb1\x8eF\xac<q\x02\xb0'x\x1d\xa5\x09\
+\x0f\x003?\xe5B\xd5\x17\xfc\x00\x1d\x7f\x15\xf4=\xf4\
+\x00e\xc1\xdf8Z\xe1\x06\xd0}v\xd4\x11\x84\xac\x82\
+\x1a`\xec\xbfn0\xf9\x84\x19\x80ES\x11\xf9\x111\
+\x00\x93\xb6:\xf2\x08-\x80\xeb\xe7`\x93\x9b\x81\x82\x15\
+\xa0\xcf\xa8~8\xa3\x22\x05`VO^F\x0a\x90f\
+\x05 tQ\x02\xcc\x98\xcd_H,0\x02l\xa7>\
+\x89\x92w\x81\x1aF\x80\x7f\xa9O\x99\xf2E\xe7a2\
+W5\x00\x00m9\x1b\xf5\xae\xca)\xf7\x02S\x84\x00\
+\xfae\xf1\xfa\xd1\xb8\x80fq[q\xf9\x94\x5c\xe1*\
+*\x0c\x00\xa0\xdf\x1f\xbayL\x10\xe7\xbc\x01\x00\xc0\xfc\
+\xd8\x05\xc1%\x87\x00\x00\xb3#\xe7-\x85\xb8\x04\x00\x18\
+8\xee&2\xe6\x13\x00\x96\xa5p{\xcbE/IJ\
+\xaf;\x03\xf8\xcc+\x80I\xa6\xe8\xeeP\xb9\x05\xb0\xa9\
+\x1f\xff\x7f\x8c\xf8\x05X+\xbc\xb4\x18-\x8e\x01t'\
+\xcd\x18O9\x06\x80\x85\x83'\xe3\x12\xcf\x00N\x96P\
+\xf2\x5c\x038\x08\xa0\xa6\xf8\x06\x18\xd9GE\xf8\x06\xb0\
+\xff\x0aH\x9c\x03\xd8\xe6R\x88\x9c\x03\xe8v\xad\xc9\x93\
+\x9c\x03\xd8F\xd13\xbc\x03<\xda\xa5\x0e\xf1\x0e\xb0\xb2\
+\x89\x0f}\xe5\x1d\xc0\xee>p\xc4=@=\xc6\xef\x02\
+\x00`\xdb\x5c\xb1\xc3=\xc0\xd0\x1a\xe0\x89{\x00\xc5\xfa\
+9H\xe3\x1e`\x11F\x8f\xe9(\x03\xbc\x0a!\xb4\x95\
+\x8b2\x80n\x09\xd0\xe6\x1f@\xb3\x04x\x89,\x80\xfa\
+\xc0\xa8\xd6}i5\x7fV=\xa5\x18\x03\xa8\xf7\xe7\x05\
+\x91U\xd3?\xcb\x9d\x9a\x1aQ\x04\xb8\xcc\x8b,\xff=\
+\x13+\x80Y\x14\x01\xde[\x1f0I\xe7\xec\x87\xb1\xd1\
+\x02S\x80\xb3\xb7\xf3;`qn\x8d\xe0\xef\x01\x8c\x01\
+\xd6v\x17c\xd1\xf3\xe5\xbbE4H\x8b$\x80\xc26\
+h\x9f\x0d\xa3\xb1$\xdb\xbb\xc0\xda\xeeZ\xfe\xb7D\xb2\
+\xb8\x0bJQ\xcd\x13\x5c[\xd2K\xfb\xfe\x92Z\x84\x85\
+\x19v\x15c\x0bp\xca\xf2[j\xbe\xdf\x864\x8f*\
+\xc0zC`\xf9\x8f\xcf\x07\xe1D(\xade\xd9\x02L\
+\x18\xe6t\x9b\xff\x02\xd2Zd\x016\xf7\xd7\xf2\xd7\x01\
+\xe1K8[\x0d1~\x17\xd8\xb8s%\xfd4\x01\x98\
+\x06\x9c\x17\x10\x10\xc0\xe6nQ\x87\xc0\xea/\xad/\x0a\
+\xcf\xa2\x0cp\xbdy\xb2M\xcf\x7f\xc8\xbc\xc2\xae\x0fQ\
+\x06\xd8\xbar\x89\x9e3\xb9\x0a\xc1\xae\x06\x04\x060c\
+T\xe1e\x9a*X\xd0\xa2\x0d\xa0oW\x81\xecy\x8a\
+\x5cM\xcc\x8aI\x12\xacw\xdbc\x1e\x12\xdb\xd9h3\
+\xeb\xe1\xb1]5\xdbcCb\xbd\xbf\x04{\x80\xddm\
+\x06\xb3\xae\x9f\x085\xb3,A1\x80\xdeJ\xac\x01\x0c\
+\x82\x18)\x97\xfdp\xf5r\x88\xad\xc5\x99\x03\x18=\xc0\
+&\xee\xdd\xfc\x85U)\xe88h\xa0\x00\xc6\xc5\x81u\
+\xe7\xd7\xee\xc5~\xc0\x05\x02\x01\x03\x80\xf1\xf5;\xeb\xb4\
+/\xfa\xd0\xb4b\xa4\x098\x00\xcc\xf6[\xae:\xb9\x16\
+\xae\xceLC\x00A\xf5\x96c\x0eP5\x9dB\xdd\xf6\
+\x91\xe0\xd6\xb4\x8221\x02,\x00\x16\xb1l\xa9j\x95\
+\xd3\xa0\xf7r\xe6\x11\x80g@\x03`\x9d\xe0\x99\xbd1\
+y6\x9e^X\xd4\xcf\x1e\x06\xd8\x5c\x8f9\x80m\x8d\
+x\xe6\xe4v\xeb\x85v\xd1\xaf[\xed8,\xfd\x00@\
+\x04\x00Nj\xc2\xe5B\xe5\xac\xd1\xe9v\xdb\x8d\xcb\xea\
+\x81M\xa5`6\xd0\xbe\xca\x01\x00\xe4\x05\xa6\xe3x\x05\
+\xc8\x00\xaa,\xa7\x9f\x1f\x03`\x03h\xb0\x9b~\xa2\x8d\
+\xb1\xaf\xf0\x03\xb3\xf9\xd7\xc2\xe8\xac\xca\x1e`\xceh\xfa\
+GS\x00\x94\x00\xeb+\xa4\x9e\x87X}\x06\xc0\x0a\xe0\
+\xbfa\x96X\x9b\x01\xe0\x05\xf0\xdb/)\xdb\x5c\x00`\
+\x06\x80\xc7o\xde\x9b\x86\xc9\xdf\xc7\x10\xea\x08&Qr\
+q\x93\xf34\xfbro\x05\xc0\x03\x00\x00L/2.\
+[\xa4\xd5\x1f5\x08\x7f\x04\x99*\xab\xb4J\xce\xee\x08\
+b\xae\xd6V\xe0cF\xc0\xb9\xc2\xfa\xb4]\xcd[)\
+\xc8\xd9Js\xb4\x82\x8f\x1b\xa1$K\xcf\x87\xdd\xab\xe3\
+\xc3b>\x9dL\xc8\x92 %R\x99\xfc\xfe\xd7\xf2i\
+\xb3?Y\xc0G\x8f(g\x8b\x13\x00\x01\x10\x00\x01\x10\
+\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\
+\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\x10\x00\x01\
+\x10\x00\x01\x10\x00\x01\x10@\x80\x00q\x1b\x04@\x00\x04\
+\xf0\x0e\x10\xdfA\x00\x04@\x00\x04\x10\xeb\xf1\x17\xe9\x89\
+Gh\xda\x1b|\x00\x00\x00\x00\x00IEND\xaeB\
+`\x82\
"
qt_resource_name = b"\
@@ -721,17 +721,17 @@ qt_resource_name = b"\
\x00s\
\x00i\x00d\x00e\x004\x00.\x00p\x00n\x00g\
\x00\x09\
-\x0a\x86\xa4\xa7\
+\x0a\x89\xa4\xa7\
\x00s\
-\x00i\x00d\x00e\x003\x00.\x00p\x00n\x00g\
+\x00i\x00d\x00e\x006\x00.\x00p\x00n\x00g\
\x00\x09\
\x0a\x85\xa4\xa7\
\x00s\
\x00i\x00d\x00e\x002\x00.\x00p\x00n\x00g\
\x00\x09\
-\x0a\x89\xa4\xa7\
+\x0a\x86\xa4\xa7\
\x00s\
-\x00i\x00d\x00e\x006\x00.\x00p\x00n\x00g\
+\x00i\x00d\x00e\x003\x00.\x00p\x00n\x00g\
"
qt_resource_struct = b"\
@@ -740,17 +740,17 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x01e\xaf\x16\xd2\x95\
-\x00\x00\x00r\x00\x00\x00\x00\x00\x01\x00\x00\x1a\x1c\
-\x00\x00\x01e\xaf\x16\xd2\x95\
-\x00\x00\x00Z\x00\x00\x00\x00\x00\x01\x00\x00\x11\x05\
-\x00\x00\x01e\xaf\x16\xd2\x95\
+\x00\x00\x01z\xe7\xee&\xf9\
+\x00\x00\x00r\x00\x00\x00\x00\x00\x01\x00\x00\x1a\x97\
+\x00\x00\x01z\xe7\xee&\xf9\
+\x00\x00\x00\x8a\x00\x00\x00\x00\x00\x01\x00\x00!\x83\
+\x00\x00\x01z\xe7\xee&\xf9\
\x00\x00\x00B\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xc3\
-\x00\x00\x01e\xaf\x16\xd2\x95\
+\x00\x00\x01z\xe7\xee&\xf9\
\x00\x00\x00*\x00\x00\x00\x00\x00\x01\x00\x00\x04\x18\
-\x00\x00\x01e\xaf\x16\xd2\x95\
-\x00\x00\x00\x8a\x00\x00\x00\x00\x00\x01\x00\x00!\x08\
-\x00\x00\x01e\xaf\x16\xd2\x95\
+\x00\x00\x01z\xe7\xee&\xf9\
+\x00\x00\x00Z\x00\x00\x00\x00\x00\x01\x00\x00\x11\x05\
+\x00\x00\x01z\xe7\xee&\xf9\
"
def qInitResources():
diff --git a/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.png b/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.png
new file mode 100644
index 000000000..263d7a3d1
--- /dev/null
+++ b/examples/opengl/threadedqopenglwidget/doc/threadedqopenglwidget.png
Binary files 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..edb88e77c
--- /dev/null
+++ b/examples/opengl/threadedqopenglwidget/glwidget.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+
+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()
+
+ @Slot()
+ 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..2a379da27
--- /dev/null
+++ b/examples/opengl/threadedqopenglwidget/main.py
@@ -0,0 +1,93 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+"""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..4999b799d
--- /dev/null
+++ b/examples/opengl/threadedqopenglwidget/mainwindow.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+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..81ec63cbb
--- /dev/null
+++ b/examples/opengl/threadedqopenglwidget/renderer.py
@@ -0,0 +1,326 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+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"]
+}