OpenGL under QML Squircle#
The OpenGL under QML example shows how an application can make use of the QQuickWindow::beforeRendering() signal to draw custom OpenGL content under a Qt Quick scene. This signal is emitted at the start of every frame, before the scene graph starts its rendering, thus any OpenGL draw calls that are made as a response to this signal, will stack under the Qt Quick items.
As an alternative, applications that wish to render OpenGL content on top of the Qt Quick scene, can do so by connecting to the QQuickWindow::afterRendering() signal.
In this example, we will also see how it is possible to have values that are exposed to QML which affect the OpenGL rendering. We animate the threshold value using a NumberAnimation in the QML file and this value is used by the OpenGL shader program that draws the squircles.
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import sys
from pathlib import Path
from PySide6.QtCore import QUrl
from PySide6.QtGui import QGuiApplication
from PySide6.QtQuick import QQuickView, QQuickWindow, QSGRendererInterface
from squircle import Squircle # noqa: F401
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
QQuickWindow.setGraphicsApi(QSGRendererInterface.OpenGL)
view = QQuickView()
view.setResizeMode(QQuickView.SizeRootObjectToView)
qml_file = Path(__file__).parent / "main.qml"
view.setSource(QUrl.fromLocalFile(qml_file))
if view.status() == QQuickView.Error:
sys.exit(-1)
view.show()
sys.exit(app.exec())
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import OpenGLUnderQML
Item {
width: 320
height: 480
Squircle {
SequentialAnimation on t {
NumberAnimation { to: 1; duration: 2500; easing.type: Easing.InQuad }
NumberAnimation { to: 0; duration: 2500; easing.type: Easing.OutQuad }
loops: Animation.Infinite
running: true
}
}
Rectangle {
color: Qt.rgba(1, 1, 1, 0.7)
radius: 10
border.width: 1
border.color: "white"
anchors.fill: label
anchors.margins: -10
}
Text {
id: label
color: "black"
wrapMode: Text.WordWrap
text: "The background here is a squircle rendered with raw OpenGL using the 'beforeRender()' signal in QQuickWindow. This text label and its border is rendered using QML"
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: 20
}
}
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import Property, QRunnable, Qt, Signal, Slot
from PySide6.QtQml import QmlElement
from PySide6.QtQuick import QQuickItem, QQuickWindow
from squirclerenderer import SquircleRenderer
# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "OpenGLUnderQML"
QML_IMPORT_MAJOR_VERSION = 1
class CleanupJob(QRunnable):
def __init__(self, renderer):
super().__init__()
self._renderer = renderer
def run(self):
del self._renderer
@QmlElement
class Squircle(QQuickItem):
tChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self._t = 0.0
self._renderer = None
self.windowChanged.connect(self.handleWindowChanged)
def t(self):
return self._t
def setT(self, value):
if self._t == value:
return
self._t = value
self.tChanged.emit()
if self.window():
self.window().update()
@Slot(QQuickWindow)
def handleWindowChanged(self, win):
if win:
win.beforeSynchronizing.connect(self.sync, type=Qt.DirectConnection)
win.sceneGraphInvalidated.connect(self.cleanup, type=Qt.DirectConnection)
win.setColor(Qt.black)
self.sync()
@Slot()
def cleanup(self):
del self._renderer
self._renderer = None
@Slot()
def sync(self):
window = self.window()
if not self._renderer:
self._renderer = SquircleRenderer()
window.beforeRendering.connect(self._renderer.init, Qt.DirectConnection)
window.beforeRenderPassRecording.connect(
self._renderer.paint, Qt.DirectConnection
)
self._renderer.setViewportSize(window.size() * window.devicePixelRatio())
self._renderer.setT(self._t)
self._renderer.setWindow(window)
def releaseResources(self):
self.window().scheduleRenderJob(
CleanupJob(self._renderer), QQuickWindow.BeforeSynchronizingStage
)
self._renderer = None
t = Property(float, t, setT, notify=tChanged)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from textwrap import dedent
import numpy as np
from OpenGL.GL import (GL_ARRAY_BUFFER, GL_BLEND, GL_DEPTH_TEST, GL_FLOAT,
GL_ONE, GL_SRC_ALPHA, GL_TRIANGLE_STRIP)
from PySide6.QtCore import QSize, Slot
from PySide6.QtGui import QOpenGLFunctions
from PySide6.QtOpenGL import QOpenGLShader, QOpenGLShaderProgram
from PySide6.QtQuick import QQuickWindow, QSGRendererInterface
VERTEX_SHADER = dedent(
"""\
attribute highp vec4 vertices;
varying highp vec2 coords;
void main() {
gl_Position = vertices;
coords = vertices.xy;
}
"""
)
FRAGMENT_SHADER = dedent(
"""\
uniform lowp float t;
varying highp vec2 coords;
void main() {
lowp float i = 1. - (pow(abs(coords.x), 4.) + pow(abs(coords.y), 4.));
i = smoothstep(t - 0.8, t + 0.8, i);
i = floor(i * 20.) / 20.;
gl_FragColor = vec4(coords * .5 + .5, i, i);
}
"""
)
class SquircleRenderer(QOpenGLFunctions):
def __init__(self):
QOpenGLFunctions.__init__(self)
self._viewport_size = QSize()
self._t = 0.0
self._program = None
self._window = QQuickWindow()
def setT(self, t):
self._t = t
def setViewportSize(self, size):
self._viewport_size = size
def setWindow(self, window):
self._window = window
@Slot()
def init(self):
if not self._program:
rif = self._window.rendererInterface()
assert (rif.graphicsApi() == QSGRendererInterface.OpenGL)
self.initializeOpenGLFunctions()
self._program = QOpenGLShaderProgram()
self._program.addCacheableShaderFromSourceCode(QOpenGLShader.Vertex, VERTEX_SHADER)
self._program.addCacheableShaderFromSourceCode(QOpenGLShader.Fragment, FRAGMENT_SHADER)
self._program.bindAttributeLocation("vertices", 0)
self._program.link()
@Slot()
def paint(self):
# Play nice with the RHI. Not strictly needed when the scenegraph uses
# OpenGL directly.
self._window.beginExternalCommands()
self._program.bind()
self._program.enableAttributeArray(0)
values = np.array([-1, -1, 1, -1, -1, 1, 1, 1], dtype="single")
# This example relies on (deprecated) client-side pointers for the vertex
# input. Therefore, we have to make sure no vertex buffer is bound.
self.glBindBuffer(GL_ARRAY_BUFFER, 0)
self._program.setAttributeArray(0, GL_FLOAT, values, 2)
self._program.setUniformValue1f("t", self._t)
self.glViewport(0, 0, self._viewport_size.width(), self._viewport_size.height())
self.glDisable(GL_DEPTH_TEST)
self.glEnable(GL_BLEND)
self.glBlendFunc(GL_SRC_ALPHA, GL_ONE)
self.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
self._program.disableAttributeArray(0)
self._program.release()
self._window.endExternalCommands()