场景图 - RHI 纹理项

演示如何实现一个自定义的 QQuickItem,用于显示由 QRhi-渲染的纹理。

此示例演示了如何实现一个项,该项使用 QRhi API 在纹理中执行跨平台的、可移植的 3D 渲染,并将该图像显示出来。

注意:此示例演示了高级、低级别的功能,可通过有限的兼容性保证的 Qt Gui 模块执行可移植、跨平台的 3D 渲染。为了能够使用 QRhi APIs,应用程序连接到 Qt::GuiPrivate 并包含 <rhi/qrhi.h>

与其他方法的比较

RHI Under QML 示例说明如何以自定义渲染先于 Qt Quick 场景图自己的渲染方式实现可移植、跨平台的 3D 渲染。这种方法是高效的,因为它现在不需要额外的渲染目标和渲染传递,自定义渲染在场景图的自己的 draw calls 之前注入到主渲染传递中。

相比之下,此示例涉及一个单独的渲染目标,一个 QRhiTexture,其 尺寸 与场景中 QQuickItem 的大小相同,以及一个用于清除并绘制到该纹理的整个渲染传递。然后在该纹理中采样,并用于纹理化四边形,有效显示 2D 图像。

与覆盖/叠加方法相比,这允许在 Qt Quick 场景中的任何位置显示、混合和转换 3D 渲染的扁平 2D 图像,因为我们有一个真正的 QQuickItem。然而,这会使资源使用和性能成本更高,因为它涉及首先对纹理进行渲染。

概览

示例使用 QQuickRhiItemQQuickRhiItemRenderer 实现。可以继承 QQuickRhiItem 以轻松快速地获得一个功能齐全的自定义 QQuickItem,并通过使用幕后 QSGSimpleTextureNode 显示 QRhiTexture 的内容。纹理的内容由应用在其 QQuickRhiItemRenderer 子类的逻辑中提供的逻辑生成。

ExampleRhiItem 是一个 QQuickRhiItem 子类,提供了一些属性,例如 anglebackgroundAlpha。这些属性将从 QML 中进行读取、写入和动画处理。为了支持 Qt Quick 的多线程渲染模型,QQQuickRhiItemRenderer 有一个临时的 synchronize() 函数,可以重新实现,以便安全地在属于主/GUI 线程的 QQuickRhiItem(属于渲染线程,如果有的话)和属于渲染线程的 QQuickRhiItemRenderer 之间复制数据。

QQuickRhiItemRenderer *ExampleRhiItem::createRenderer()
{
    return new ExampleRhiItemRenderer;
}

void ExampleRhiItem::setAngle(float a)
{
    if (m_angle == a)
        return;

    m_angle = a;
    emit angleChanged();
    update();
}

void ExampleRhiItem::setBackgroundAlpha(float a)
{
    if (m_alpha == a)
        return;

    m_alpha = a;
    emit backgroundAlphaChanged();
    update();
}

void ExampleRhiItemRenderer::synchronize(QQuickRhiItem *rhiItem)
{
    ExampleRhiItem *item = static_cast<ExampleRhiItem *>(rhiItem);
    if (item->angle() != m_angle)
        m_angle = item->angle();
    if (item->backgroundAlpha() != m_alpha)
        m_alpha = item->backgroundAlpha();
}

initialize() 函数在第一次调用 render() 之前至少被调用一次,但在实际应用中可能被多次调用:如果 QQuickItem 几何形状改变(由于某些布局更改、调整窗口大小等),如果如样本数和纹素格式之类的 QQuickRhiItem 设置改变,或者如果项目重新分层使它属于一个新的 QQuickWindow,这些都会触发再次调用 initialize(),因为它们意味着一或多个资源 QQuickRhiItem 管理的资源改变,这对子类也有影响。这里提供的示例代码准备处理这些特殊情况(更改 QRhi、更改样本数、更改纹素格式)。(因为它没有保留用作颜色缓冲区的纹素,因此当纹素因不同大小而重新创建时无需特殊处理)

void ExampleRhiItemRenderer::initialize(QRhiCommandBuffer *cb)
{
    if (m_rhi != rhi()) {
        m_rhi = rhi();
        m_pipeline.reset();
    }

    if (m_sampleCount != renderTarget()->sampleCount()) {
        m_sampleCount = renderTarget()->sampleCount();
        m_pipeline.reset();
    }

    QRhiTexture *finalTex = m_sampleCount > 1 ? resolveTexture() : colorTexture();
    if (m_textureFormat != finalTex->format()) {
        m_textureFormat = finalTex->format();
        m_pipeline.reset();
    }

initialize() 的其余部分是简单的 QRhi-相关代码。

3D 场景使用透视投影,该投影基于查询自 QRhiRenderTarget 的输出大小进行计算(因为这无论是否使用多重采样都有效,而访问 colorTexture() 和 msaaColorBuffer() 都需要根据哪些对象有效进行分支逻辑)

注意使用 QRhi::clipSpaceCorrMatrix() 以适应用户界面坐标系统和 3D 图形 API 之间的坐标系统差异。

    if (!m_pipeline) {
        m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
        m_vbuf->create();

        m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
        m_ubuf->create();

        m_srb.reset(m_rhi->newShaderResourceBindings());
        m_srb->setBindings({
            QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, m_ubuf.get()),
        });
        m_srb->create();

        m_pipeline.reset(m_rhi->newGraphicsPipeline());
        m_pipeline->setShaderStages({
           { QRhiShaderStage::Vertex, getShader(QLatin1String(":/scenegraph/rhitextureitem/shaders/color.vert.qsb")) },
           { QRhiShaderStage::Fragment, getShader(QLatin1String(":/scenegraph/rhitextureitem/shaders/color.frag.qsb")) }
        });
        QRhiVertexInputLayout inputLayout;
        inputLayout.setBindings({
            { 5 * sizeof(float) }
        });
        inputLayout.setAttributes({
            { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
            { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
        });
        m_pipeline->setSampleCount(m_sampleCount);
        m_pipeline->setVertexInputLayout(inputLayout);
        m_pipeline->setShaderResourceBindings(m_srb.get());
        m_pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
        m_pipeline->create();

        QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
        resourceUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData);
        cb->resourceUpdate(resourceUpdates);
    }

    const QSize outputSize = renderTarget()->pixelSize();
    m_viewProjection = m_rhi->clipSpaceCorrMatrix();
    m_viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f);
    m_viewProjection.translate(0, 0, -4);

render() 的实现记录了单独三角形的绘制。每次都会更新包含 4x4 矩阵的统一缓冲区,因为我们预期旋转角度会改变。清除颜色包含项目提供的背景不透明度。记住需要将红、绿和蓝组件中的不透明度预乘。

void ExampleRhiItemRenderer::render(QRhiCommandBuffer *cb)
{
    QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
    QMatrix4x4 modelViewProjection = m_viewProjection;
    modelViewProjection.rotate(m_angle, 0, 1, 0);
    resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData());

    // Qt Quick expects premultiplied alpha
    const QColor clearColor = QColor::fromRgbF(0.5f * m_alpha, 0.5f * m_alpha, 0.7f * m_alpha, m_alpha);
    cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates);

    cb->setGraphicsPipeline(m_pipeline.get());
    const QSize outputSize = renderTarget()->pixelSize();
    cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height()));
    cb->setShaderResources();
    const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
    cb->setVertexInput(0, 1, &vbufBinding);
    cb->draw(3);

    cb->endPass();
}

示例项目 @ code.qt.io

另请参阅 QQuickRhiItem场景图 - QML 下的 RHI场景图 - 自定义 QSGRenderNode

© 2024 Qt 公司。本文档的文档贡献的版权归各自所有者所有。提供在本文档中的文档是根据自由软件基金会发布的 GNU 自由文档许可协议版本 1.3 的条款许可的。Qt 和相应的标志是芬兰和/或其他国家/地区的 The Qt Company Ltd. 的商标。所有其他商标均为其各自所有者财产。