diff options
author | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2021-10-07 12:47:46 +0200 |
---|---|---|
committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2021-10-11 22:23:14 +0000 |
commit | a3f40e25ebe81804bf1e19d5ee64108d4304c9ab (patch) | |
tree | b551e78c52b8f4015019e1df2797d3efc9afe802 | |
parent | 3c77475375209125a53f2950c2413cec7f18c8c5 (diff) |
Port the rendercontrol_opengl/rendercontrol_opengl example
Task-number: PYSIDE-841
Change-Id: I432498ff85d346def9604a551fddddb0a0939b25
Reviewed-by: Christian Tismer <tismer@stackless.com>
(cherry picked from commit ba1bb6785ba2d3b4569b7bce4c98ed07f73bb1b8)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
6 files changed, 807 insertions, 0 deletions
diff --git a/examples/declarative/rendercontrol/rendercontrol_opengl/cuberenderer.py b/examples/declarative/rendercontrol/rendercontrol_opengl/cuberenderer.py new file mode 100644 index 000000000..90cd78e65 --- /dev/null +++ b/examples/declarative/rendercontrol/rendercontrol_opengl/cuberenderer.py @@ -0,0 +1,223 @@ +############################################################################# +## +## 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$ +## +############################################################################# + +import ctypes +import numpy +from OpenGL.GL import (GL_COLOR_BUFFER_BIT, GL_CULL_FACE, GL_CW, + GL_DEPTH_BUFFER_BIT, GL_DEPTH_TEST, GL_FALSE, GL_FLOAT, + GL_TEXTURE_2D, GL_TRIANGLES) + +from PySide6.QtGui import (QMatrix4x4, QOffscreenSurface, QOpenGLContext, + QOpenGLFunctions, QWindow) +from PySide6.QtOpenGL import (QOpenGLBuffer, QOpenGLShader, + QOpenGLShaderProgram, QOpenGLVertexArrayObject) +from shiboken6 import VoidPtr + + +VERTEXSHADER_SOURCE = """attribute highp vec4 vertex; +attribute lowp vec2 coord; +varying lowp vec2 v_coord; +uniform highp mat4 matrix; +void main() { + v_coord = coord; + gl_Position = matrix * vertex; +} +""" + + +FRAGMENTSHADER_SOURCE = """varying lowp vec2 v_coord; +uniform sampler2D sampler; +void main() { + gl_FragColor = vec4(texture2D(sampler, v_coord).rgb, 1.0); +} +""" + + +FLOAT_SIZE = ctypes.sizeof(ctypes.c_float) + + +VERTEXES = numpy.array([-0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, + 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, + 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, + + 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, 0.5, -0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, + -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, + -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, + + 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, + -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, + -0.5, -0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, + 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5, 0.5], + dtype=numpy.float32) + + +TEX_COORDS = numpy.array([0.0, 0.0, 1.0, 1.0, 1.0, 0.0, + 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, + 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, + 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, + + 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, + 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, + 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, + 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, + + 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, + 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, + 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 1.0, 1.0], dtype=numpy.float32) + + +class CubeRenderer(): + def __init__(self, offscreenSurface): + self.m_angle = 0 + self.m_offscreenSurface = offscreenSurface + self.m_context = None + self.m_program = None + self.m_vbo = None + self.m_vao = None + self.m_matrixLoc = 0 + self.m_proj = QMatrix4x4() + + def __del__(self): + # Use a temporary offscreen surface to do the cleanup. There may not + # be a native window surface available anymore at self stage. + self.m_context.makeCurrent(self.m_offscreenSurface) + del self.m_program + del self.m_vbo + del self.m_vao + self.m_context.doneCurrent() + + def init(self, w, share): + self.m_context = QOpenGLContext() + self.m_context.setShareContext(share) + self.m_context.setFormat(w.requestedFormat()) + self.m_context.create() + if not self.m_context.makeCurrent(w): + return + + f = self.m_context.functions() + f.glClearColor(0.0, 0.1, 0.25, 1.0) + f.glViewport(0, 0, w.width() * w.devicePixelRatio(), + w.height() * w.devicePixelRatio()) + + self.m_program = QOpenGLShaderProgram() + self.m_program.addCacheableShaderFromSourceCode(QOpenGLShader.Vertex, + VERTEXSHADER_SOURCE) + self.m_program.addCacheableShaderFromSourceCode(QOpenGLShader.Fragment, + FRAGMENTSHADER_SOURCE) + self.m_program.bindAttributeLocation("vertex", 0) + self.m_program.bindAttributeLocation("coord", 1) + self.m_program.link() + self.m_matrixLoc = self.m_program.uniformLocation("matrix") + + self.m_vao = QOpenGLVertexArrayObject() + self.m_vao.create() + vaoBinder = QOpenGLVertexArrayObject.Binder(self.m_vao) + + self.m_vbo = QOpenGLBuffer() + self.m_vbo.create() + self.m_vbo.bind() + + vertexCount = 36 + self.m_vbo.allocate(FLOAT_SIZE * vertexCount * 5) + vertex_data = VERTEXES.tobytes() + tex_coord_data = TEX_COORDS.tobytes() + self.m_vbo.write(0, VoidPtr(vertex_data), + FLOAT_SIZE * vertexCount * 3) + self.m_vbo.write(FLOAT_SIZE * vertexCount * 3, + VoidPtr(tex_coord_data), + FLOAT_SIZE * vertexCount * 2) + self.m_vbo.release() + + if self.m_vao.isCreated(): + self.setupVertexAttribs() + + def resize(self, w, h): + self.m_proj.setToIdentity() + self.m_proj.perspective(45, w / float(h), 0.01, 100.0) + + def setupVertexAttribs(self): + self.m_vbo.bind() + self.m_program.enableAttributeArray(0) + self.m_program.enableAttributeArray(1) + f = self.m_context.functions() + + null = VoidPtr(0) + pointer = VoidPtr(36 * 3 * FLOAT_SIZE) + f.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, null) + f.glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, pointer) + self.m_vbo.release() + + def render(self, w, share, texture): + if not self.m_context: + self.init(w, share) + + if not self.m_context.makeCurrent(w): + return + + f = self.m_context.functions() + f.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + + if texture: + f.glBindTexture(GL_TEXTURE_2D, texture) + f.glFrontFace(GL_CW) # because our cube's vertex data is such + f.glEnable(GL_CULL_FACE) + f.glEnable(GL_DEPTH_TEST) + + self.m_program.bind() + vaoBinder = QOpenGLVertexArrayObject.Binder(self.m_vao) + # If VAOs are not supported, set the vertex attributes every time. + if not self.m_vao.isCreated(): + self.setupVertexAttribs() + + m = QMatrix4x4() + m.translate(0, 0, -2) + m.rotate(90, 0, 0, 1) + m.rotate(self.m_angle, 0.5, 1, 0) + self.m_angle += 0.5 + + self.m_program.setUniformValue(self.m_matrixLoc, self.m_proj * m) + + # Draw the cube. + f.glDrawArrays(GL_TRIANGLES, 0, 36) + + self.m_context.swapBuffers(w) diff --git a/examples/declarative/rendercontrol/rendercontrol_opengl/demo.qml b/examples/declarative/rendercontrol/rendercontrol_opengl/demo.qml new file mode 100644 index 000000000..aeffc7646 --- /dev/null +++ b/examples/declarative/rendercontrol/rendercontrol_opengl/demo.qml @@ -0,0 +1,208 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt for Python examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import QtQuick.Particles 2.0 + +Rectangle { + id: root + + gradient: Gradient { + GradientStop { position: 0; color: mouse.pressed ? "lightsteelblue" : "steelblue" } + GradientStop { position: 1; color: "black" } + } + + Text { + anchors.centerIn: parent + text: "Qt Quick in a texture" + font.pointSize: 40 + color: "white" + + SequentialAnimation on rotation { + PauseAnimation { duration: 2500 } + NumberAnimation { from: 0; to: 360; duration: 5000; easing.type: Easing.InOutCubic } + loops: Animation.Infinite + } + } + + ParticleSystem { + id: particles + anchors.fill: parent + + ImageParticle { + id: smoke + system: particles + anchors.fill: parent + groups: ["A", "B"] + source: "qrc:///particleresources/glowdot.png" + colorVariation: 0 + color: "#00111111" + } + ImageParticle { + id: flame + anchors.fill: parent + system: particles + groups: ["C", "D"] + source: "qrc:///particleresources/glowdot.png" + colorVariation: 0.1 + color: "#00ff400f" + } + + Emitter { + id: fire + system: particles + group: "C" + + y: parent.height + width: parent.width + + emitRate: 350 + lifeSpan: 3500 + + acceleration: PointDirection { y: -17; xVariation: 3 } + velocity: PointDirection {xVariation: 3} + + size: 24 + sizeVariation: 8 + endSize: 4 + } + + TrailEmitter { + id: fireSmoke + group: "B" + system: particles + follow: "C" + width: root.width + height: root.height - 68 + + emitRatePerParticle: 1 + lifeSpan: 2000 + + velocity: PointDirection {y:-17*6; yVariation: -17; xVariation: 3} + acceleration: PointDirection {xVariation: 3} + + size: 36 + sizeVariation: 8 + endSize: 16 + } + + TrailEmitter { + id: fireballFlame + anchors.fill: parent + system: particles + group: "D" + follow: "E" + + emitRatePerParticle: 120 + lifeSpan: 180 + emitWidth: TrailEmitter.ParticleSize + emitHeight: TrailEmitter.ParticleSize + emitShape: EllipseShape{} + + size: 16 + sizeVariation: 4 + endSize: 4 + } + + TrailEmitter { + id: fireballSmoke + anchors.fill: parent + system: particles + group: "A" + follow: "E" + + emitRatePerParticle: 128 + lifeSpan: 2400 + emitWidth: TrailEmitter.ParticleSize + emitHeight: TrailEmitter.ParticleSize + emitShape: EllipseShape{} + + velocity: PointDirection {yVariation: 16; xVariation: 16} + acceleration: PointDirection {y: -16} + + size: 24 + sizeVariation: 8 + endSize: 8 + } + + Emitter { + id: balls + system: particles + group: "E" + + y: parent.height + width: parent.width + + emitRate: 2 + lifeSpan: 7000 + + velocity: PointDirection {y:-17*4*2; xVariation: 6*6} + acceleration: PointDirection {y: 17*2; xVariation: 6*6} + + size: 8 + sizeVariation: 4 + } + + Turbulence { //A bit of turbulence makes the smoke look better + anchors.fill: parent + groups: ["A","B"] + strength: 32 + system: particles + } + } + + onWidthChanged: particles.reset() + onHeightChanged: particles.reset() + + MouseArea { + id: mouse + anchors.fill: parent + } +} diff --git a/examples/declarative/rendercontrol/rendercontrol_opengl/doc/rendercontrol_opengl.rst b/examples/declarative/rendercontrol/rendercontrol_opengl/doc/rendercontrol_opengl.rst new file mode 100644 index 000000000..f47567f52 --- /dev/null +++ b/examples/declarative/rendercontrol/rendercontrol_opengl/doc/rendercontrol_opengl.rst @@ -0,0 +1,5 @@ +QQuickRenderControl OpenGL Example +================================== + +The QQuickRenderControl OpenGL Example shows how to render a Qt Quick scene into a +texture that is then used by a non-Quick based OpenGL renderer. diff --git a/examples/declarative/rendercontrol/rendercontrol_opengl/main.py b/examples/declarative/rendercontrol/rendercontrol_opengl/main.py new file mode 100644 index 000000000..84a857838 --- /dev/null +++ b/examples/declarative/rendercontrol/rendercontrol_opengl/main.py @@ -0,0 +1,57 @@ +############################################################################# +## +## 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$ +## +############################################################################# + +import sys +from PySide6.QtGui import QGuiApplication +from PySide6.QtQuick import QQuickWindow, QSGRendererInterface + +from window_singlethreaded import WindowSingleThreaded + + +if __name__ == "__main__": + app = QGuiApplication(sys.argv) + # only functional when Qt Quick is also using OpenGL + QQuickWindow.setGraphicsApi(QSGRendererInterface.OpenGLRhi) + window = WindowSingleThreaded() + window.resize(1024, 768) + window.show() + ex = app.exec() + del window + sys.exit(ex) diff --git a/examples/declarative/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pyproject b/examples/declarative/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pyproject new file mode 100644 index 000000000..b2e80ab23 --- /dev/null +++ b/examples/declarative/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pyproject @@ -0,0 +1,6 @@ +{ + "files": ["cuberenderer.py", + "main.py", + "window_singlethreaded.py", + "demo.qml"] +} diff --git a/examples/declarative/rendercontrol/rendercontrol_opengl/window_singlethreaded.py b/examples/declarative/rendercontrol/rendercontrol_opengl/window_singlethreaded.py new file mode 100644 index 000000000..08b3236a2 --- /dev/null +++ b/examples/declarative/rendercontrol/rendercontrol_opengl/window_singlethreaded.py @@ -0,0 +1,308 @@ +############################################################################# +## +## 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$ +## +############################################################################# + +import numpy +from pathlib import Path +import sys +import weakref +from OpenGL.GL import (GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER, + GL_NEAREST, GL_RGBA, GL_TEXTURE_2D, GL_UNSIGNED_BYTE) + +from PySide6.QtGui import (QMatrix4x4, QMouseEvent, QOffscreenSurface, + QOpenGLContext, QOpenGLFunctions, QScreen, QSurface, + QSurfaceFormat, QWindow) +from PySide6.QtOpenGL import (QOpenGLFramebufferObject, QOpenGLTexture, + QOpenGLShaderProgram, QOpenGLVertexArrayObject, + QOpenGLBuffer) +from PySide6.QtQml import QQmlComponent, QQmlEngine +from PySide6.QtQuick import (QQuickGraphicsDevice, + QQuickItem, QQuickRenderControl, + QQuickRenderTarget, QQuickWindow) +from PySide6.QtCore import QCoreApplication, QTimer, QUrl +from shiboken6 import VoidPtr + +from cuberenderer import CubeRenderer + + +class RenderControl(QQuickRenderControl): + def __init__(self, window=None): + super().__init__() + self._window = window + + def renderWindow(self, offset): + return self._window() # Dereference the weak reference + + +class WindowSingleThreaded(QWindow): + + def __init__(self): + super().__init__() + self.m_rootItem = None + self.m_device = None + self.m_texture_ids = numpy.array([0], dtype=numpy.uint32) + + self.m_quickInitialized = False + self.m_quickReady = False + self.m_dpr = 0 + self.m_status_conn_id = None + self.setSurfaceType(QSurface.OpenGLSurface) + + format = QSurfaceFormat() + # Qt Quick may need a depth and stencil buffer. Always make sure these + # are available. + format.setDepthBufferSize(16) + format.setStencilBufferSize(8) + self.setFormat(format) + + self.m_context = QOpenGLContext() + self.m_context.setFormat(format) + self.m_context.create() + + self.m_offscreenSurface = QOffscreenSurface() + # Pass m_context.format(), not format. Format does not specify and + # color buffer sizes, while the context, that has just been created, + # reports a format that has these values filled in. Pass self to the + # offscreen surface to make sure it will be compatible with the + # context's configuration. + self.m_offscreenSurface.setFormat(self.m_context.format()) + self.m_offscreenSurface.create() + + self.m_cubeRenderer = CubeRenderer(self.m_offscreenSurface) + + self.m_renderControl = RenderControl(weakref.ref(self)) + + # Create a QQuickWindow that is associated with out render control. + # Note that this window never gets created or shown, meaning that + # will never get an underlying native (platform) window. + self.m_quickWindow = QQuickWindow(self.m_renderControl) + + # Create a QML engine. + self.m_qmlEngine = QQmlEngine() + if not self.m_qmlEngine.incubationController(): + c = self.m_quickWindow.incubationController() + self.m_qmlEngine.setIncubationController(c) + + # When Quick says there is a need to render, we will not render + # immediately. Instead, a timer with a small interval is used + # to get better performance. + self.m_updateTimer = QTimer() + self.m_updateTimer.setSingleShot(True) + self.m_updateTimer.setInterval(5) + self.m_updateTimer.timeout.connect(self.render) + + # Now hook up the signals. For simplicy we don't differentiate between + # renderRequested (only render is needed, no sync) and sceneChanged + # (polish and sync is needed too). + self.m_quickWindow.sceneGraphInitialized.connect(self.createTexture) + self.m_quickWindow.sceneGraphInvalidated.connect(self.destroyTexture) + self.m_renderControl.renderRequested.connect(self.requestUpdate) + self.m_renderControl.sceneChanged.connect(self.requestUpdate) + + # Just recreating the texture on resize is not sufficient, when moving + # between screens with different devicePixelRatio the QWindow size may + # remain the same but the texture dimension is to change regardless. + self.screenChanged.connect(self.handleScreenChange) + + def __del__(self): + # Make sure the context is current while doing cleanup. Note that + # we use the offscreen surface here because passing 'self' at self + # point is not safe: the underlying platform window may already be + # destroyed. To avoid all the trouble, use another surface that is + # valid for sure. + self.m_context.makeCurrent(self.m_offscreenSurface) + + del self.m_qmlComponent + del self.m_qmlEngine + del self.m_quickWindow + del self.m_renderControl + + if self.texture_id(): + self.m_context.functions().glDeleteTextures(1, self.m_texture_ids) + + self.m_context.doneCurrent() + + def texture_id(self): + return self.m_texture_ids[0] + + def set_texture_id(self, texture_id): + self.m_texture_ids[0] = texture_id + + def createTexture(self): + # The scene graph has been initialized. It is now time to create a + # texture and associate it with the QQuickWindow. + self.m_dpr = self.devicePixelRatio() + self.m_textureSize = self.size() * self.m_dpr + f = self.m_context.functions() + f.glGenTextures(1, self.m_texture_ids) + f.glBindTexture(GL_TEXTURE_2D, self.texture_id()) + + f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + null = VoidPtr(0) + f.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.m_textureSize.width(), + self.m_textureSize.height(), 0, + GL_RGBA, GL_UNSIGNED_BYTE, null) + target = QQuickRenderTarget.fromOpenGLTexture(self.texture_id(), + self.m_textureSize) + self.m_quickWindow.setRenderTarget(target) + + def destroyTexture(self): + self.m_context.functions().glDeleteTextures(1, self.m_texture_ids) + self.set_texture_id(0) + + def render(self): + if not self.m_context.makeCurrent(self.m_offscreenSurface): + return + + # Polish, synchronize and render the next frame (into our texture). + # In this example everything happens on the same thread and therefore + # all three steps are performed in succession from here. In a threaded + # setup the render() call would happen on a separate thread. + self.m_renderControl.beginFrame() + self.m_renderControl.polishItems() + self.m_renderControl.sync() + self.m_renderControl.render() + self.m_renderControl.endFrame() + + QOpenGLFramebufferObject.bindDefault() + self.m_context.functions().glFlush() + + self.m_quickReady = True + + # Get something onto the screen. + texture_id = self.texture_id() if self.m_quickReady else 0 + self.m_cubeRenderer.render(self, self.m_context, texture_id) + + def requestUpdate(self): + if not self.m_updateTimer.isActive(): + self.m_updateTimer.start() + + def run(self): + if self.m_status_conn_id: + self.m_qmlComponent.statusChanged.disconnect(self.m_status_conn_id) + self.m_status_conn_id = None + + if self.m_qmlComponent.isError(): + for error in self.m_qmlComponent.errors(): + print(error.url().toString(), error.line(), error.toString()) + return + + self.m_rootItem = self.m_qmlComponent.create() + if self.m_qmlComponent.isError(): + for error in self.m_qmlComponent.errors(): + print(error.url().toString(), error.line(), error.toString()) + return + + if not self.m_rootItem: + print("run: Not a QQuickItem") + del self.m_rootItem + + # The root item is ready. Associate it with the window. + self.m_rootItem.setParentItem(self.m_quickWindow.contentItem()) + + # Update item and rendering related geometries. + self.updateSizes() + + # Initialize the render control and our OpenGL resources. + self.m_context.makeCurrent(self.m_offscreenSurface) + self.m_device = QQuickGraphicsDevice.fromOpenGLContext(self.m_context) + self.m_quickWindow.setGraphicsDevice(self.m_device) + self.m_renderControl.initialize() + self.m_quickInitialized = True + + def updateSizes(self): + # Behave like SizeRootObjectToView. + w = self.width() + h = self.height() + self.m_rootItem.setWidth(w) + self.m_rootItem.setHeight(h) + self.m_quickWindow.setGeometry(0, 0, w, h) + self.m_cubeRenderer.resize(w, h) + + def startQuick(self, filename): + url = QUrl.fromLocalFile(filename) + self.m_qmlComponent = QQmlComponent(self.m_qmlEngine, url) + if self.m_qmlComponent.isLoading(): + self.m_status_conn_id = self.m_qmlComponent.statusChanged.connect(self.run) + else: + self.run() + + def exposeEvent(self, event): + if self.isExposed() and not self.m_quickInitialized: + texture_id = self.texture_id() if self.m_quickReady else 0 + self.m_cubeRenderer.render(self, self.m_context, texture_id) + qml_file = Path(__file__).parent / "demo.qml" + self.startQuick(qml_file) + + def resizeTexture(self): + if self.m_rootItem and self.m_context.makeCurrent(self.m_offscreenSurface): + self.m_context.functions().glDeleteTextures(1, self.m_texture_ids) + self.set_texture_id(0) + self.createTexture() + self.m_context.doneCurrent() + self.updateSizes() + self.render() + + def resizeEvent(self, event): + # If self is a resize after the scene is up and running, recreate the + # texture and the Quick item and scene. + if (self.texture_id() + and self.m_textureSize != self.size() * self.devicePixelRatio()): + self.resizeTexture() + + def handleScreenChange(self): + if self.m_dpr != self.devicePixelRatio(): + self.resizeTexture() + + def mousePressEvent(self, e): + # Use the constructor taking position and globalPosition. That puts + # position into the event's position and scenePosition, and + # globalPosition into the event's globalPosition. This way the + # scenePosition in `e` is ignored and is replaced by position. + # This is necessary because QQuickWindow thinks of itself as + # a top-level window always. + mappedEvent = QMouseEvent(e.type(), e.position(), e.globalPosition(), + e.button(), e.buttons(), e.modifiers()) + QCoreApplication.sendEvent(self.m_quickWindow, mappedEvent) + + def mouseReleaseEvent(self, e): + mappedEvent = QMouseEvent(e.type(), e.position(), e.globalPosition(), + e.button(), e.buttons(), e.modifiers()) + QCoreApplication.sendEvent(self.m_quickWindow, mappedEvent) |