场景图——QML中的OpenGL

演示如何在Qt Quick 场景中渲染OpenGL内容。

在 <span translate=Qt Quick 场景中使用 OpenGL 渲染的带文字叠加的动画“椭圆”" src="images/openglunderqml-example.jpg" title="在 Qt Quick 场景中使用 OpenGL 渲染的带文字叠加的动画“椭圆”"/>

QML下的OpenGL示例演示了应用程序如何利用QQuickWindow::beforeRendering()信号,在Qt Quick 场景下绘制自定义的OpenGL内容。该信号在每帧开始时发出,即在场景图开始渲染之前,因此作为对该信号响应而发出的任何OpenGL绘制调用,都将堆叠在Qt Quick 项之下。

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

在本示例中,我们还将了解如何将影响 OpenGL 渲染的值暴露给 QML。我们通过 QML 文件中的 `NumberAnimation ` 对阈值进行动画处理,而绘制“松鼠圈”的 OpenGL 着色器程序会使用该值。

该示例在多数方面与“Direct3D 11 Under QML”、“Metal Under QML ”和“Vulkan 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;
    QOpenGLBuffer m_vbo;
};

接着,我们需要一个负责渲染的对象。该实例必须与QQuickItem 分离,因为该项位于GUI线程中,而渲染可能在渲染线程中进行。由于我们需要连接到QQuickWindow::beforeRendering(),因此将渲染器设为QObject 。渲染器包含其所需的所有状态副本,且独立于GUI线程。

注意:切勿 试图将这两个对象合并为一个。在渲染线程进行渲染时,QQuickItems 可能会在 GUI 线程上被删除。

接下来我们进入实现部分。

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

Squircle 类的构造函数仅需初始化相关值,并连接到窗口的“changed”信号——我们将利用该信号来准备渲染器。

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

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

        m_vbo.create();
        m_vbo.bind();
        m_vbo.allocate(values, sizeof(values));

        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);

        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_vbo.bind();
    m_program->bind();
    m_program->setUniformValue("t", (float)m_t);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);

    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);

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

    m_window->endExternalCommands();
}

我们在paint() 中使用着色器程序来绘制椭圆形。

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

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.