简单的 RHI 小工具示例

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

简单 RHI Widget 示例截图

该示例在很多方面都与QWidget 世界中的RHI 窗口示例相对应。此应用程序中的QRhiWidget 子类使用带有基本顶点和片段着色器的简单图形流水线渲染单个三角形。与基于QWindow 的普通应用程序不同,该示例无需关心低级细节,如设置窗口和QRhi ,或处理 swapchain 和窗口事件,因为QWidget 框架会处理这些工作。QRhiWidget 子类的实例被添加到QVBoxLayout 中。为了使示例保持最小化和紧凑,没有引入更多的 widget 或 3D 内容。

一旦ExampleRhiWidgetQRhiWidget 子类)的实例被添加到顶层 widget 的子层级结构中,相应的窗口就会自动成为 Direct 3D、Vulkan、Metal 或 OpenGL 渲染的窗口。QPainter 渲染的部件内容,即所有不是QRhiWidgetQOpenGLWidgetQQuickWidget 的内容,都会上载到纹理中,而上述特殊部件则各自渲染到纹理中。由此产生的一组textures 将由顶层 widget 的后备存储合成在一起。

结构和 main()

main() 函数非常简单。顶层 widget 的默认大小为 720p(此大小为逻辑单位,实际像素大小可能不同,具体取决于scale factor )。窗口大小可调整。QRhiWidget 可以轻松实现子类,正确处理因窗口大小或布局变化而导致的 widget 大小调整。

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 实例不为 widget 所拥有。它将在initialize() from the base class 中被查询。将其存储为一个成员可以在再次调用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 系列应用程序接口兼容性承诺的更多详情,请参阅QRhi

CMakeLists.txt

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

渲染设置

examplewidget.cpp 中,widget 实现使用辅助函数从.qsb 文件加载QShader 对象。该应用程序通过 Qt 资源系统将预设的.qsb 文件嵌入到可执行文件中。由于模块依赖性(以及仍支持 qmake),本示例没有使用方便的 CMake 函数qt_add_shaders() ,而是将.qsb 文件作为源代码树的一部分。我们鼓励现实世界中的应用程序避免这种做法,而是使用 Qt XMLShader 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();
}

让我们看看初始化()的实现。首先,查询并存储QRhi 对象,以备日后使用,同时也便于在今后调用函数时进行比较。当出现不匹配时(例如,当窗口部件在窗口之间移动时),需要重新创建的图形资源将通过销毁和清空一个合适的对象(在本例中为m_pipeline )来触发。本示例并未主动演示窗口之间的重新分配,但已做好处理准备。示例还准备处理窗口大小调整时可能发生的部件尺寸变化。这不需要特殊处理,因为每次发生这种情况时都会调用initialize() ,因此查询renderTarget()->pixelSize()colorTexture()->pixelSize() 总能得到最新的像素大小。本示例不支持更改纹理格式和multisample settings ,因为它只使用默认设置(RGBA8 和无多重采样抗锯齿)。

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

当需要(重新)创建图形资源时,initialize() 会使用非常典型的基于QRhi 的代码来完成。一个包含交错位置-颜色顶点数据的顶点缓冲区就足够了,而模型视图-投影矩阵则通过一个 64 字节(16 个浮点)的统一缓冲区显示。统一缓冲区是唯一的着色器可见资源,而且只在顶点着色器中使用。图形管道依赖于大量默认设置(例如,关闭深度测试、禁用混合、启用颜色写入、禁用面剔除、三角形默认拓扑结构等)。顶点数据布局为x,y,r,g,b ,因此跨距为 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 报告的尺寸或设备像素比例手动计算尺寸、视口、剪刀等。

注: 投影矩阵包括correction matrixQRhi ,以适应 3D API 在归一化设备坐标方面的差异。(例如,Y 向下与 Y 向上)。

应用-4 的平移只是为了确保z 值为 0 的三角形是可见的。

    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 对象并不罕见,只要它与管道创建时给出的layout-compatible 。没有索引缓冲区,只有一个顶点缓冲区绑定(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

另请参见 QRhiCube RHI Widget 示例RHI Window 示例

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