diff options
Diffstat (limited to 'examples/quick/rendercontrol/rendercontrol_opengl')
6 files changed, 647 insertions, 0 deletions
diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/cuberenderer.py b/examples/quick/rendercontrol/rendercontrol_opengl/cuberenderer.py new file mode 100644 index 000000000..69e7321f9 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_opengl/cuberenderer.py @@ -0,0 +1,183 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +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, QOpenGLContext +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() + + 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() + # 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/quick/rendercontrol/rendercontrol_opengl/demo.qml b/examples/quick/rendercontrol/rendercontrol_opengl/demo.qml new file mode 100644 index 000000000..00f6a81e9 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_opengl/demo.qml @@ -0,0 +1,161 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +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/quick/rendercontrol/rendercontrol_opengl/doc/rendercontrol_opengl.rst b/examples/quick/rendercontrol/rendercontrol_opengl/doc/rendercontrol_opengl.rst new file mode 100644 index 000000000..f47567f52 --- /dev/null +++ b/examples/quick/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/quick/rendercontrol/rendercontrol_opengl/main.py b/examples/quick/rendercontrol/rendercontrol_opengl/main.py new file mode 100644 index 000000000..ee885ae6d --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_opengl/main.py @@ -0,0 +1,20 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +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/quick/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pyproject b/examples/quick/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pyproject new file mode 100644 index 000000000..b2e80ab23 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pyproject @@ -0,0 +1,6 @@ +{ + "files": ["cuberenderer.py", + "main.py", + "window_singlethreaded.py", + "demo.qml"] +} diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py b/examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py new file mode 100644 index 000000000..6f1e61f94 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_opengl/window_singlethreaded.py @@ -0,0 +1,272 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import numpy +from pathlib import Path +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 (QMouseEvent, QOffscreenSurface, + QOpenGLContext, QSurface, + QSurfaceFormat, QWindow) +from PySide6.QtOpenGL import QOpenGLFramebufferObject +from PySide6.QtQml import QQmlComponent, QQmlEngine +from PySide6.QtQuick import (QQuickGraphicsDevice, + QQuickRenderControl, + QQuickRenderTarget, QQuickWindow) +from PySide6.QtCore import QCoreApplication, QTimer, QUrl, Slot +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 + + @Slot() + 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) + + @Slot() + def destroyTexture(self): + self.m_context.functions().glDeleteTextures(1, self.m_texture_ids) + self.set_texture_id(0) + + @Slot() + 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() + + @Slot() + 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) |