场景图 - 自定义 QSGRenderNode

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

自定义渲染节点示例展示如何实现一个 QQuickItem 子类,后面有一个由 QSGRenderNode 派生的场景图节点支撑,并且提供它自己的基于 QRhi-的渲染。

注意:此示例演示了高级、低级别的功能,执行便携式、跨平台的 3D 渲染,并依赖于从 Qt Gui 模块提供有限兼容性保证的 API。要使用 QRhi API,应用程序需要链接到 Qt::GuiPrivate 并包含 <rhi/qrhi.h>

QSGRenderNode 允许直接访问场景图中的渲染硬件接口 (RHI)。此示例演示了如何创建基于 QSGRenderNode 的渲染节点,并通过自定义项目来管理它。渲染节点创建了一个 RHI 管道,更新顶点和统一缓冲区,并将渲染到 RHI 命令缓冲区。

在实践中,这是一种便携式、跨平台的自定义渲染方法,与场景图本身的渲染并行执行,而无需求助于原生 3D API(如 OpenGL、Metal 或 Vulkan)。相反,应用程序使用 Qt 的图形和着色器抽象层。

QSGRenderNode 是将自定义 2D/3D 渲染集成到 Qt Quick 场景的三种途径之一。其他两种选择是在 Qt Quick 场景的渲染之前或之后执行渲染,或者生成一个针对专用渲染目标(一个纹理)的整个单独的渲染通道,然后场景中的项目显示该纹理。基于 QSGRenderNode 的方法与前者类似,因为不涉及额外的渲染通道或渲染目标,并允许将自定义渲染命令“内联”注入到 Qt Quick 场景的渲染中。

请参考以下示例了解这三种方法

  • 场景图 - RHI 在 QML 下 - 演示了一个基于 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标志设置为指示这是一个可视项。

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

另请参阅QSGRenderNodeQRhi场景图 - QML下的RHI场景图 - RHI纹理项Qt Quick场景图

© 2024 The Qt Company Ltd. 本文档中的文档贡献的版权属于其各自的所有者。提供的文档受GNU自由文档许可证版本1.3的条款约束,由自由软件基金会发布。Qt和相关标志是The Qt Company Ltd在芬兰和其他国家/地区的商标。所有其他商标均为其各自所有者的财产。