场景图 - QML 下的 OpenGL

展示如何在 Qt Quick 场景下渲染 OpenGL 方法。

在 QML 下的 OpenGL 示例展示了如何通过 QQuickWindow::beforeRendering() 信号在 Qt Quick 场景下绘制自定义 OpenGL 内容。该信号在每个帧的开始处发出,在场景图开始渲染之前,因此任何对该信号的反应所进行的 OpenGL 绘制调用都将堆叠在 Qt Quick 项目之下。

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

在本示例中,我们还将了解如何让暴露给 QML 的值影响 OpenGL 渲染。我们使用 QML 文件中的一个 NumberAnimation 动画来动画化阈值值,该值被用于绘制 squircles 的 OpenGL 着色器程序中。

从大多数方面来看,该示例与 Direct3D 11 Under QMLMetal Under QMLVulkan Under QML 示例等效,它们都渲染相同自定义内容,只是通过不同的本地 API。

class Squircle : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged)
    QML_ELEMENT

public:
    Squircle();

    qreal t() const { return m_t; }
    void setT(qreal t);

signals:
    void tChanged();

public slots:
    void sync();
    void cleanup();

private slots:
    void handleWindowChanged(QQuickWindow *win);

private:
    void releaseResources() override;

    qreal m_t;
    SquircleRenderer *m_renderer;
};

首先,我们需要一个我们可以暴露给 QML 的对象。这是一个 QQuickItem 的子类,因此我们可以轻松访问 QQuickItem::window()。我们使用 QML_ELEMENT 宏将其暴露给 QML。

class SquircleRenderer : public QObject, protected QOpenGLFunctions
{
    Q_OBJECT
public:
    ~SquircleRenderer();

    void setT(qreal t) { m_t = t; }
    void setViewportSize(const QSize &size) { m_viewportSize = size; }
    void setWindow(QQuickWindow *window) { m_window = window; }

public slots:
    void init();
    void paint();

private:
    QSize m_viewportSize;
    qreal m_t = 0.0;
    QOpenGLShaderProgram *m_program = nullptr;
    QQuickWindow *m_window = nullptr;
};

然后,我们需要一个处理渲染的对象。这个实例需要与 QQuickItem 分离,因为项目位于 GUI 线程,而渲染可能发生在渲染线程上。由于我们想连接到 QQuickWindow::beforeRendering(),我们将渲染器生成一个 QObject。渲染器包含它需要的所有状态副本,与 GUI 线程独立。

注意:不要尝试将这两个对象合并成一个。QQuickItems 可能会在 GUI 线程中被删除,而渲染线程正在渲染。

让我们进入实现步骤。

Squircle::Squircle()
    : m_t(0)
    , m_renderer(nullptr)
{
    connect(this, &QQuickItem::windowChanged, this, &Squircle::handleWindowChanged);
}

Squircle 类的构造函数只是初始化值并连接到我们将用它来准备渲染器的窗口更改信号。

void Squircle::handleWindowChanged(QQuickWindow *win)
{
    if (win) {
        connect(win, &QQuickWindow::beforeSynchronizing, this, &Squircle::sync, Qt::DirectConnection);
        connect(win, &QQuickWindow::sceneGraphInvalidated, this, &Squircle::cleanup, Qt::DirectConnection);

一旦我们有了窗口,我们就连接到 QQuickWindow::beforeSynchronizing() 信号,我们将使用它来创建渲染器并将状态安全地复制到其中。我们还连接到 QQuickWindow::sceneGraphInvalidated() 信号来处理渲染器的清理。

注意:由于 Squircle 对象与 GUI 线程相关联,而信号由渲染线程发出,因此使用 Qt::DirectConnection 连接至关重要。否则,槽函数将在错误的线程上调用,并且没有 OpenGL 上下文。

        // Ensure we start with cleared to black. The squircle's blend mode relies on this.
        win->setColor(Qt::black);
    }
}

场景图的默认行为是在渲染之前清除帧缓冲区。这没有问题,因为我们将在清除入列后插入自己的渲染代码。不过,请确保我们清除到期望的颜色(黑色)。

void Squircle::sync()
{
    if (!m_renderer) {
        m_renderer = new SquircleRenderer();
        connect(window(), &QQuickWindow::beforeRendering, m_renderer, &SquircleRenderer::init, Qt::DirectConnection);
        connect(window(), &QQuickWindow::beforeRenderPassRecording, m_renderer, &SquircleRenderer::paint, Qt::DirectConnection);
    }
    m_renderer->setViewportSize(window()->size() * window()->devicePixelRatio());
    m_renderer->setT(m_t);
    m_renderer->setWindow(window());
}

我们使用 sync() 函数来初始化渲染器并将我们的项目的状态复制到渲染器中。当创建渲染器时,我们还连接了 QQuickWindow::beforeRendering() 和 QQuickWindow::beforeRenderPassRecording() 到渲染器的 init()paint() 槽。

注意:QQuickWindow::beforeSynchronizing() 信号在渲染线程上发出,而 GUI 线程被阻塞,因此可以安全地简单地复制值,无需任何额外的保护。

void Squircle::cleanup()
{
    delete m_renderer;
    m_renderer = nullptr;
}

class CleanupJob : public QRunnable
{
public:
    CleanupJob(SquircleRenderer *renderer) : m_renderer(renderer) { }
    void run() override { delete m_renderer; }
private:
    SquircleRenderer *m_renderer;
};

void Squircle::releaseResources()
{
    window()->scheduleRenderJob(new CleanupJob(m_renderer), QQuickWindow::BeforeSynchronizingStage);
    m_renderer = nullptr;
}

SquircleRenderer::~SquircleRenderer()
{
    delete m_program;
}

cleanup() 函数中,我们删除了渲染器,这会反过来清理其自己的资源。这通过重新实现 QQuickWindow::releaseResources() 来补充,因为仅仅连接到 sceneGraphInvalidated() 信号本身不足以处理所有情况。

void Squircle::setT(qreal t)
{
    if (t == m_t)
        return;
    m_t = t;
    emit tChanged();
    if (window())
        window()->update();
}

t 的值改变时,我们调用 QQuickWindow::update() 而不是 QQuickItem::update(),因为前者将强制整个窗口重新绘制,即使自上一帧以来场景图没有变化。

void SquircleRenderer::init()
{
    if (!m_program) {
        QSGRendererInterface *rif = m_window->rendererInterface();
        Q_ASSERT(rif->graphicsApi() == QSGRendererInterface::OpenGL);

        initializeOpenGLFunctions();

        m_program = new QOpenGLShaderProgram();
        m_program->addCacheableShaderFromSourceCode(QOpenGLShader::Vertex,
                                                    "attribute highp vec4 vertices;"
                                                    "varying highp vec2 coords;"
                                                    "void main() {"
                                                    "    gl_Position = vertices;"
                                                    "    coords = vertices.xy;"
                                                    "}");
        m_program->addCacheableShaderFromSourceCode(QOpenGLShader::Fragment,
                                                    "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);"
                                                    "}");

        m_program->bindAttributeLocation("vertices", 0);
        m_program->link();

    }
}

在 SquircleRenderer 的 init() 函数中,我们首先初始化着色器程序(如果尚未完成)。槽函数被调用的线程上当前是 OpenGL 上下文。

void SquircleRenderer::paint()
{
    // Play nice with the RHI. Not strictly needed when the scenegraph uses
    // OpenGL directly.
    m_window->beginExternalCommands();

    m_program->bind();

    m_program->enableAttributeArray(0);

    float values[] = {
        -1, -1,
        1, -1,
        -1, 1,
        1, 1
    };

    // This example relies on (deprecated) client-side pointers for the vertex
    // input. Therefore, we have to make sure no vertex buffer is bound.
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    m_program->setAttributeArray(0, GL_FLOAT, values, 2);
    m_program->setUniformValue("t", (float) m_t);

    glViewport(0, 0, m_viewportSize.width(), m_viewportSize.height());

    glDisable(GL_DEPTH_TEST);

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    m_program->disableAttributeArray(0);
    m_program->release();

    m_window->endExternalCommands();
}

我们使用着色器程序在 paint() 中绘制 squircle。

int main(int argc, char **argv)
{
    QGuiApplication app(argc, argv);

    QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);

    QQuickView view;
    view.setResizeMode(QQuickView::SizeRootObjectToView);
    view.setSource(QUrl("qrc:///scenegraph/openglunderqml/main.qml"));
    view.show();

    return QGuiApplication::exec();
}

应用程序的 main() 函数实例化一个 QQuickView 并启动 main.qml 文件。

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
        }
    }

我们使用在 main() 函数中注册的名称导入 Squircle QML 类型。然后我们实例化它,并为其 t 属性创建一个运行中的 NumberAnimation

    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: qsTr("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
    }
}

然后我们叠加一段简短的描述性文本,以便清楚地显示我们实际上在 Qt Quick 场景下渲染 OpenGL。

示例项目 @ code.qt.io

© 2024 Qt 公司有限公司。此处包含的文档贡献权属于其各自的拥有者。本体内的文档是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 的条款授予的。Qt 和相关标志是芬兰和/或全球其他地区的 The Qt 公司商标。所有其他商标均为各自所有者的财产。