场景图 - 自定义 QSGRenderNode

演示如何使用QSGRenderNode 在Qt Quick 场景图中实现自定义渲染。

该自定义渲染节点示例演示了如何实现一个QQuickItem 子类,该子类由继承自QSGRenderNode 的场景图节点提供支持,并提供基于QRhi 的自定义渲染功能。

在“直接”和“图层”渲染模式下显示的包含重叠矩形的RGB三角形

注意:本示例 演示了执行可移植、跨平台 3D 渲染的高级低级功能,同时依赖于 Qt GUI 模块中兼容性保证有限的 API。为了能够使用QRhi API,应用程序需链接到Qt::GuiPrivate 并包含<rhi/qrhi.h> 。

QSGRenderNode 允许在场景图内直接访问渲染硬件接口(RHI)。本示例演示了如何创建基于QSGRenderNode 的渲染节点,并通过自定义项对其进行管理。该渲染节点会创建一个RHI管道,更新顶点和统一缓冲区,并将结果渲染到RHI命令缓冲区中。

实际上,这是一种可移植的、跨平台的方法,可在不依赖 OpenGL、Metal 或 Vulkan 等原生 3D API 的情况下,与场景图自身的渲染流程并行执行自定义渲染。相反,应用程序使用 Qt 的图形和着色器抽象层。

QSGRenderNode 这是将自定义 2D/3D 渲染集成到Qt Quick 场景中的三种方式之一。另外两种选项分别是:在before 或after 处对Qt Quick 场景的自身渲染进行处理,或者生成一个完全独立的渲染通道,将其目标指向专用的渲染目标(纹理),然后让场景中的某个项显示该纹理。 基于QSGRenderNode 的方法与前一种类似,即不涉及额外的渲染通道或渲染目标,并且允许在Qt Quick 场景自身的渲染过程中“内联”注入自定义渲染命令。

关于这三种方法,请参考以下示例:

  • 场景图 - QML下的RHI- 演示了基于QQuickWindow::beforeRendering()信号的“底层”方法。无需额外的渲染通道和资源,但与Qt Quick 场景其余部分的合成和混合能力相当有限。在Qt Quick 场景“下方”或“上方”进行渲染是最简单的方法。
  • 场景图 - RHI 纹理项- 演示了如何创建一个自定义的QQuickItem ,该项将内容渲染到纹理中,并显示一个带有生成的内容纹理的四边形。这种方法非常灵活,允许将生成的 2D 图像与Qt Quick 场景的其余部分进行完全的混合和合成。但代价是需要额外的渲染阶段和渲染目标。
  • 此示例——演示了“内联”方法,即在主渲染通道期间,Qt Quick 场景图会调用自定义项和节点的实现。这种方法在性能方面表现优异(无需额外的渲染通道,也不涉及纹理贴图和混合),但存在潜在的陷阱,且是复杂度最高的方法。

自定义项继承自QQuickItem 。最重要的是,它重写了updatePaintNode()方法。

class CustomRender : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(QList<QVector2D> vertices READ vertices WRITE setVertices NOTIFY verticesChanged)
    QML_ELEMENT

public:
    explicit CustomRender(QQuickItem *parent = nullptr);

    QList<QVector2D> vertices() const;
    void setVertices(const QList<QVector2D> &newVertices);

signals:
    void verticesChanged();

protected:
    QSGNode *updatePaintNode(QSGNode *old, UpdatePaintNodeData *) override;

private:
    QList<QVector2D> m_vertices;
};

构造函数将ItemHasContents 标志设置为true,以指示这是一个视觉项。

CustomRender::CustomRender(QQuickItem *parent)
    : QQuickItem(parent)
{
    setFlag(ItemHasContents, true);
    connect(this, &CustomRender::verticesChanged, this, &CustomRender::update);
}

updatePaintNode() 的实现会创建一个自定义场景图节点的实例(如果尚未创建)。该项的底层QSGNode 树由单个节点组成,即QSGRenderNode 派生类的实例。 当使用Qt Quick 的线程化渲染模型时,该函数将在渲染线程上被调用,而主线程会被阻塞。因此,访问主线程数据(例如存储在QQuickItems中的数据)是安全的。该节点(即QSGRenderNode 子类的实例)将在渲染线程上“存活”。

QSGNode *CustomRender::updatePaintNode(QSGNode *old, UpdatePaintNodeData *)
{
    CustomRenderNode *node = static_cast<CustomRenderNode *>(old);

    if (!node)
        node = new CustomRenderNode(window());

    node->setVertices(m_vertices);

    return node;
}

CustomRenderNode 类继承自QSGRenderNode ,并重写了若干虚函数。 在管理QRhi 资源(缓冲区、管道等)时,智能指针在此情况下非常有用,因为节点会由场景图在渲染线程(若存在)上与场景的其余部分一同销毁,而此时QRhi 仍可用;因此,通过析构函数或智能指针释放资源是合法且安全的。

class CustomRenderNode : public QSGRenderNode
{
public:
    CustomRenderNode(QQuickWindow *window);

    void setVertices(const QList<QVector2D> &vertices);

    void prepare() override;
    void render(const RenderState *state) override;
    void releaseResources() override;
    RenderingFlags flags() const override;
    QSGRenderNode::StateFlags changedStates() const override;

protected:
    QQuickWindow *m_window;
    std::unique_ptr<QRhiBuffer> m_vertexBuffer;
    std::unique_ptr<QRhiBuffer> m_uniformBuffer;
    std::unique_ptr<QRhiShaderResourceBindings> m_resourceBindings;
    std::unique_ptr<QRhiGraphicsPipeline> m_pipeline;
    QList<QRhiShaderStage> m_shaders;
    bool m_verticesDirty = true;
    QList<QVector2D> m_vertices;
};

行为良好的 `QSGRenderNode ` 子类还会重写 `releaseResources()` 方法,在此情况下,该方法可以仅包含一组简单的 `reset()` 调用。

void CustomRenderNode::releaseResources()
{
    m_vertexBuffer.reset();
    m_uniformBuffer.reset();
    m_pipeline.reset();
    m_resourceBindings.reset();
}

该QSGRenderNode 通过QRhi API(而非直接通过OpenGL、Vulkan、Metal等)进行渲染,并且会考虑项的变换(因为它实际上只进行2D渲染)。因此,指定适当的标志可能会带来微小的性能提升。

QSGRenderNode::RenderingFlags CustomRenderNode::flags() const
{
    // We are rendering 2D content directly into the scene graph using QRhi, no
    // direct usage of a 3D API. Hence NoExternalRendering. This is a minor
    // optimization.

    // Additionally, the node takes the item transform into account by relying
    // on projectionMatrix() and matrix() (see prepare()) and never rendering at
    // other Z coordinates. Hence DepthAwareRendering. This is a potentially
    // bigger optimization.

    return QSGRenderNode::NoExternalRendering | QSGRenderNode::DepthAwareRendering;
}

每次渲染Qt Quick 场景时,都会调用`prepare()`和`render()`函数。前者在准备渲染通道(但尚未开始录制)时被调用。通常,该函数会创建尚未准备好的资源(如缓冲区、纹理和图形管道),并将数据上传至这些资源的队列中。

void CustomRenderNode::prepare()
{
    QRhi *rhi = m_window->rhi();
    QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch();

    if (m_verticesDirty) {
        m_vertexBuffer.reset();
        m_verticesDirty = false;
    }

    if (!m_vertexBuffer) {
        m_vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer,
                                            m_vertices.count() * sizeof(QVector2D)));
        m_vertexBuffer->create();
        resourceUpdates->uploadStaticBuffer(m_vertexBuffer.get(), m_vertices.constData());
    }

render() 函数在渲染通道录制过程中被调用,其目标要么是QQuickWindow 的交换链,要么是纹理(适用于分层对象,或位于ShaderEffectSource 内时)。

void CustomRenderNode::render(const RenderState *)
{
    QRhiCommandBuffer *cb = commandBuffer();
    cb->setGraphicsPipeline(m_pipeline.get());
    QSize renderTargetSize = renderTarget()->pixelSize();
    cb->setViewport(QRhiViewport(0, 0, renderTargetSize.width(), renderTargetSize.height()));
    cb->setShaderResources();
    QRhiCommandBuffer::VertexInput vertexBindings[] = { { m_vertexBuffer.get(), 0 } };
    cb->setVertexInput(0, 1, vertexBindings);
    cb->draw(m_vertices.count());
}

示例项目 @ code.qt.io

另请参阅 QSGRenderNode 、QRhi 、QML 下的 RHI 场景图、 RHI 纹理项以及Qt Quick 场景图。

© 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.