简单的 RHI 小部件示例
演示了如何使用QRhi (Qt 3D API及着色语言抽象层)渲染一个三角形。

“简单 RHI 小部件”示例的屏幕截图
从许多方面来看,该示例相当于QWidget 世界中的“RHI窗口示例”。 该应用程序中的QRhiWidget 子类使用包含基本顶点着色器和片段着色器的简单图形管道来渲染一个三角形。与基于QWindow 的普通应用程序不同,此示例无需担心低级细节,例如设置窗口和QRhi ,或处理交换链和窗口事件,因为这些工作已由QWidget 框架处理。QRhiWidget 子类的实例被添加到QVBoxLayout 中。为了使示例保持简洁紧凑,这里没有引入其他控件或3D内容。
一旦将ExampleRhiWidget (QRhiWidget 的子类)的实例添加到顶级小部件的子元素层次结构中,相应的窗口就会自动变为由Direct 3D、Vulkan、Metal或OpenGL渲染的窗口。 随后,由QPainter 渲染的小部件内容(即所有非QRhiWidget 、QOpenGLWidget 或QQuickWidget 的内容)会被上传到纹理中,而上述特殊小部件则各自渲染到各自的纹理中。生成的textures 集合由顶级小部件的backingstore进行合成。
结构与 main()
main() 函数非常简单。顶级控件默认尺寸为720p(该尺寸以逻辑单位表示,实际像素尺寸可能因scale factor 而异)。该窗口支持调整大小。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() 和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 系列 API 的兼容性承诺的更多详细信息,请参阅QRhi 。
CMakeLists.txt
target_link_libraries(simplerhiwidget PRIVATE
Qt6::Core
Qt6::Gui
Qt6::GuiPrivate
Qt6::Widgets
)渲染设置
在examplewidget.cpp 中,控件实现使用了一个辅助函数,从.qsb 文件中加载QShader 对象。 该应用程序随附了预先配置好的.qsb 文件,这些文件通过Qt资源系统嵌入到可执行文件中。由于模块依赖关系(以及仍需支持qmake),本示例并未使用便捷的CMake函数qt_add_shaders() ,而是将.qsb 文件作为源代码树的一部分提供。 建议实际应用中避免这种做法,转而使用 QtShader 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() 总能获得最新的、实时更新的像素尺寸。 但本示例未针对color buffer formats 和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 个浮点数的偏移量(跳过x 和y )。每个图形管道都必须与一个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 报告的尺寸或设备像素比例手动计算尺寸、视口、剪切等参数。
注意: 投影矩阵 包含来自QRhi 的correction matrix ,以适应不同 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();
}另请参阅 QRhi 、Cube RHI 控件示例以及RHI 窗口示例。
© 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.