简单RHI小部件示例

演示如何使用QRhi,Qt的3D API和着色语言抽象层来渲染三角形。

简单RHI小部件示例截图

这个示例在许多方面都是QWidget世界中的RHI窗口示例的对立面。在这个应用中,QRhiWidget的子类渲染一个三角形,使用简单的图形管道和基本的顶点着色器与片元着色器。与基于QWindow的普通应用程序不同,这个示例不需要担心更低级的细节,例如设置窗口和QRhi,或处理交换链和窗口事件,因为QWidget框架已经处理了这些。将QRhiWidget子类的实例添加到QVBoxLayout。为了保持示例的简约和紧凑,没有引入其他小部件或3D内容。

一旦将QRhiWidget的子类的一个实例添加到顶级小部件的子层次结构中,相关的窗口就会自动成为一个Direct 3D、Vulkan、Metal或OpenGL渲染窗口。使用QPainter渲染的小部件内容,即除了QRhiWidgetQOpenGLWidgetQQuickWidget之外的所有内容,将被上传到一个纹理中,而上述特殊小部件则各自渲染到一个纹理中。顶级小部件的后端存储会将这些生成的纹理组合在一起。

结构和主函数

main()函数非常简单。顶级小部件默认大小为720p(此大小为逻辑单元,实际像素大小可能不同,取决于缩放因子。窗口可调整大小。QRhiWidget使实现正确处理小部件因窗口大小或布局变化而调整大小的子类变得简单。

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

    ExampleRhiWidget *rhiWidget = new ExampleRhiWidget;

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(rhiWidget);

    QWidget w;
    w.setLayout(layout);
    w.resize(1280, 720);
    w.show();

    return app.exec();
}

QRhiWidget子类重写两个虚函数:initialize()和render()。initialize()在render()之前至少调用一次,但在许多重要更改时也会被调用,例如,由于小部件的背景纹理因小部件尺寸变化而重建,或当小部件因移动到新的顶级窗口而切换到新的QRhi时。

注意:QOpenGLWidget的遗留initializeGL - resizeGL - paintGL模式不同,QRhiWidget中只有两个虚函数。这是因为除了调整大小之外,还需要处理更多特殊事件,例如将窗口重新设置到一个不同的顶级窗口中。(鲁棒QOpenGLWidget的实现必须通过执行额外的簿记来处理,例如通过跟踪相关的QOpenGLContext的生存周期,这意味着三个虚函数实际上是不够的)一对简单的initialize - render,其中initialize在重要更改时会重新调用,更适合此。

QRhi实例不由小部件拥有。它将在initialize() 从基类中查询。将其存储为成员使得在再次调用initialize()时可以识别变化。然而,图形资源,如顶点和统一缓冲区,或图形管线,则由ExampleRhiWidget控制。

#include <QRhiWidget>
#include <rhi/qrhi.h>

class ExampleRhiWidget : public QRhiWidget
{
public:
    ExampleRhiWidget(QWidget *parent = nullptr) : QRhiWidget(parent) { }

    void initialize(QRhiCommandBuffer *cb) override;
    void render(QRhiCommandBuffer *cb) override;

private:
    QRhi *m_rhi = nullptr;
    std::unique_ptr<QRhiBuffer> m_vbuf;
    std::unique_ptr<QRhiBuffer> m_ubuf;
    std::unique_ptr<QRhiShaderResourceBindings> m_srb;
    std::unique_ptr<QRhiGraphicsPipeline> m_pipeline;
    QMatrix4x4 m_viewProjection;
    float m_rotation = 0.0f;
};

为了使#include <rhi/qrhi.h>语句工作,应用程序必须链接到GuiPrivate(或使用qmake的gui-private)。有关QRhi API家族兼容性承诺的更多详细信息,请参阅QRhi

CMakeLists.txt

target_link_libraries(simplerhiwidget PRIVATE
    Qt6::Core
    Qt6::Gui
    Qt6::GuiPrivate
    Qt6::Widgets
)

渲染设置

examplewidget.cpp中,小部件实现使用辅助函数从.qsb文件加载QShader对象。此应用程序为Qt资源系统嵌入的已预处理.qsb文件提供先决条件。由于模块依赖性(以及由于仍支持qmake),此示例不使用方便的CMake功能qt_add_shaders(),而是将.qsb文件作为源树的一部分提供。鼓励真实世界的应用程序避免这样做,转而使用Qt Shader Tools模块的CMake集成功能(qt_add_shaders)。无论采用何种方法,在C++代码中,捆绑/生成.qsb文件的加载方式是相同的。

static QShader getShader(const QString &name)
{
    QFile f(name);
    return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
}

现在我们看看initialize()实现。首先,查询并存储QRhi对象以供稍后使用,并允许在函数未来调用时进行比较。当出现不匹配(例如,当小部件在不同窗口之间移动时),需要重新创建图形资源的触发器是通过销毁和空出合适对象触发的,在这种情况下是m_pipeline。此示例没有积极地展示窗口之间的重新父化,但它已经准备好处理它。它也准备好了处理当窗口调整大小时可能发生的更改小部件的大小。这不需要特殊处理,因为每次发生这种情况,都会调用initialize(),因此查询renderTarget()->pixelSize()colorTexture()->pixelSize()始终给出最新的像素大小的最新值。这个示例没有准备好处理更改纹理格式和多采样设置,因为它始终只使用默认值(RGBA8没有多采样抗锯齿)。

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

当需要(重新)创建图形资源时,initialize() 使用相当典型的基于 QRhi 的代码来完成。只需一个包含交错位置-颜色顶点数据的单个顶点缓冲区就足够了,而模型视图投影矩阵通过一个 64 字节的统一缓冲区(16 个浮点数)公开。统一缓冲区是唯一的着色器可见资源,并且只用于顶点着色器。图形管道依赖于许多默认设置(例如,深度测试关闭,混合关闭,颜色写入打开,面剔除关闭,三角形默认拓扑等)。顶点数据布局是 xyrgb,因此步长为 5 个浮点数,而第二个顶点输入属性(颜色)的偏移量为 2 个浮点数(跳过 xy)。每个图形管道必须与一个 QRhiRenderPassDescriptor 关联。这可以从基类管理的 QRhiRenderTarget 中检索。

注意:此示例依赖于 QRhiWidget 的默认 autoRenderTarget 设置为 true。这就是为什么它不需要管理渲染目标,而只需通过调用 renderTarget() 查询现有的一个。

    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(":/shader_assets/color.vert.qsb")) },
            { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/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->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);
    }

最后,计算投影矩阵。这取决于小工具的大小,因此无条件地在每个函数调用的每一步中进行。

注意:任何大小和视口计算应仅依赖于从颜色缓冲区作为资源查询的像素尺寸,因为那是实际渲染目标。避免根据 QWidget 报告的大小或设备像素比例手动计算大小、视口、剪裁等。

注意:投影矩阵包括 校正矩阵,以适应 3D API 在规范化设备坐标中的差异。(例如,Y 向下与 Y 向上)

应用 -4 的平移以确保具有 0 的 z 值的三角形可见。

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

渲染

小工具记录单个渲染通道,包含单个绘制调用。

初始化步骤中计算的视投影矩阵与模型矩阵结合,而在这个例子中,模型矩阵是一个简单的旋转。然后将该矩阵写入统一缓冲区。注意 resourceUpdates 是传递给 beginPass() 的,这是一个不需要手动调用 resourceUpdate() 的快捷方式。

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

在渲染通道中,记录单个带有 3 个顶点的绘制调用。初始化步骤中创建的图形管道绑定在命令缓冲区上,视口设置为覆盖整个小工具。要使统一缓冲区对(顶点)着色器可见,使用没有参数的 setShaderResources() 调用,这意味着使用 m_srb,因为那时它在管道创建时与管道相关联。在更复杂的渲染器中,传递不同的 QRhiShaderResourceBindings 对象并不罕见,只要它与创建管道时给出的那个布局兼容。没有索引缓冲区,只有一个顶点缓冲区绑定(vbufBinding 中的单个元素引用创建管道时指定的 QRhiVertexInputLayout 绑定列表中的单个条目)。

    const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f);
    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();

一旦渲染流程被记录,就会调用update()。这会请求一个新的帧,并用于确保小部件持续更新,三角形会旋转显示。默认情况下,渲染线程(在这种情况下为主线程)被演示速率限制。这个例子中没有适当的动画系统,因此旋转会在每一帧增加,这意味着在不同刷新率的显示上,三角形的旋转速度会不同。

    update();
}

示例项目 @ code.qt.io

另请参阅 QRhi立方RHI小部件示例RHI窗口示例

© 2024 Qt公司有限公司。本文件中包含的文档贡献属于各自的拥有者。所提供的文档符合由自由软件基金会发布的GNU自由文档许可证(FDL)1.3版的条款。Qt和相应的标志是芬兰及/或其他国家的Qt公司注册的商标。所有其他商标属于各自的拥有者。