QML下的OpenGL Squircle#

QML下的OpenGL示例显示了如何应用使用QQuickWindow::beforeRendering()信号在Qt Quick场景下绘制自定义OpenGL内容。此信号在每个框架开始时发出,在场景图开始渲染之前,因此对响应此信号而做出的任何OpenGL绘图调用都将堆叠在Qt Quick项目之下。

作为替代方案,希望将OpenGL内容渲染在Qt Quick场景之上应用程序可以通过连接到QQuickWindow::afterRendering()信号来做到这一点。

在这个示例中,我们还将看到如何让值暴露给QML,这些值会影响OpenGL渲染。我们使用QML文件中的NumberAnimation动画阈值值,该值由绘制圆圈的OpenGL着色器程序使用。

Squircle Screenshot

下载 示例

# 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()