场景图——基于QML的RHI
演示如何在Qt Quick 场景下使用QRhi 进行直接渲染。

简介
“RHI Under QML”示例演示了应用程序如何利用QQuickWindow::beforeRendering()和QQuickWindow::beforeRenderPassRecording()信号,在Qt Quick 场景下绘制基于QRhi 的自定义内容。
对于希望在Qt Quick 场景之上渲染QRhi 内容的应用程序,请使用QQuickWindow::beforeRendering() 将数据上传到缓冲区,并连接到QQuickWindow::afterRenderPassRecording() 信号。
在此示例中,我们还将了解如何将暴露给 QML 的值应用到基于QRhi 的渲染中。我们通过 QML 文件中的NumberAnimation 对阈值进行动画处理,然后将该浮点值通过统一缓冲区传递给片段着色器。
该示例在多数方面与“QML下的OpenGL”、“QML下的Direct3D 11”、“QML下的Metal”以及“QML下的Vulkan”示例相当。 那些示例通过直接使用3D API来渲染相同的内容。而本示例则完全跨平台且可移植,因为它本质上支持使用QRhi 所支持的所有3D API(例如OpenGL、Vulkan、Metal、Direct3D 11和12)。
注意:本示例 演示了用于实现可移植、跨平台 3D 渲染的高级低级功能,同时依赖于 Qt GUI 模块中兼容性保证有限的 API。为了能够使用QRhi 中的 API,应用程序需链接到Qt::GuiPrivate 并包含<rhi/qrhi.h> 。
将自定义渲染作为底层/覆盖层添加,是将自定义 2D/3D 渲染集成到Qt Quick 场景中的三种方式之一。 另外两种方法分别是:使用QSGRenderNode 与Qt Quick 场景自身的渲染进行“内联”渲染;或者生成一个完全独立的渲染通道,将其目标指向专用的渲染目标(纹理),然后让场景中的某个项显示该纹理。 关于这些方法,请参考“场景图 - RHI 纹理项”和“场景图 - 自定义 QSGRenderNode”示例。
核心概念
beforeRendering() 信号在每个帧开始时触发,即在场景图开始渲染之前,因此任何作为对此信号响应而发出的QRhi 绘制调用,都将堆叠在Qt Quick 项之下。 但是,这里有两个相关的信号:应用程序自身的QRhi 命令应记录到场景图所使用的同一命令缓冲区中,而且,这些命令应属于同一渲染通道。 仅靠 beforeRendering() 还不够,因为它是在帧开始时触发的,而此时尚未通过QRhiCommandBuffer::beginPass() 开始记录渲染通道。通过同时连接到 beforeRenderPassRecording(),应用程序自身的命令和场景图自身的渲染最终将按正确顺序排列:
- 场景图的渲染循环会调用QRhi::beginFrame()
- QQuickWindow::beforeRendering() 事件被触发——应用程序为其自定义渲染准备资源
- 场景图调用QRhiCommandBuffer::beginPass()
- QQuickWindow::beforeRenderPassRecording() 被触发——应用程序记录绘制调用
- 场景图记录绘制调用
操作指南
自定义渲染被封装在自定义的QQuickItem 中。RhiSquircle 继承自QQuickItem ,并暴露给QML(请注意QML_ELEMENT )。QML场景实例化了RhiSquircle 。但请注意,这并非可视化项:QQuickItem::ItemHasContents 标志未被设置。因此,该项的位置和大小无关紧要,且它未重写updatePaintNode()方法。
class RhiSquircle : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged)
QML_ELEMENT
public:
RhiSquircle();
qreal t() const { return m_t; }
void setT(qreal t);
signals:
void tChanged();
public slots:
void sync();
void cleanup();
private slots:
void handleWindowChanged(QQuickWindow *win);
private:
void releaseResources() override;
qreal m_t = 0;
SquircleRenderer *m_renderer = nullptr;
};相反,当该项与QQuickWindow 建立关联时,它会连接到QQuickWindow::beforeSynchronizing()信号。使用Qt::DirectConnection 非常重要,因为如果存在Qt Quick 渲染线程,该信号将在该线程上发出。我们希望连接的槽函数也在同一线程上被调用。
RhiSquircle::RhiSquircle()
{
connect(this, &QQuickItem::windowChanged, this, &RhiSquircle::handleWindowChanged);
}
void RhiSquircle::handleWindowChanged(QQuickWindow *win)
{
if (win) {
connect(win, &QQuickWindow::beforeSynchronizing, this, &RhiSquircle::sync, Qt::DirectConnection);
connect(win, &QQuickWindow::sceneGraphInvalidated, this, &RhiSquircle::cleanup, Qt::DirectConnection);
// Ensure we start with cleared to black. The squircle's blend mode relies on this.
win->setColor(Qt::black);
}
}在场景图的同步阶段,如果尚未创建,将创建渲染基础设施,并同步与渲染相关的数据,即从位于主线程上的RhiSquircle 项复制到位于渲染线程上的SquircleRenderer 对象。 (如果不存在渲染线程,则这两个对象都位于主线程上)访问数据是安全的,因为在渲染线程执行其同步阶段时,主线程会被阻塞。有关场景图线程和渲染模型的更多信息,请参阅Qt Quick Scene Graph。
除了t 的值之外,相关的QQuickWindow 指针也会被复制。虽然SquircleRenderer 即使在渲染线程上操作时,也能查询RhiSquircle 项的window(),但理论上这并不完全安全。因此需要进行复制。
在配置SquircleRenderer 时,会建立与beforeRendering()和beforeRenderPassRecording()的连接,这些连接是能够在适当时候执行操作并注入应用程序自定义3D渲染命令的关键。
void RhiSquircle::sync()
{
// This function is invoked on the render thread, if there is one.
if (!m_renderer) {
m_renderer = new SquircleRenderer;
// Initializing resources is done before starting to record the
// renderpass, regardless of wanting an underlay or overlay.
connect(window(), &QQuickWindow::beforeRendering, m_renderer, &SquircleRenderer::frameStart, Qt::DirectConnection);
// Here we want an underlay and therefore connect to
// beforeRenderPassRecording. Changing to afterRenderPassRecording
// would render the squircle on top (overlay).
connect(window(), &QQuickWindow::beforeRenderPassRecording, m_renderer, &SquircleRenderer::mainPassRecordingStart, Qt::DirectConnection);
}
m_renderer->setT(m_t);
m_renderer->setWindow(window());
}当触发beforeRendering() 时,如果尚未创建,系统会创建自定义渲染所需的QRhi 资源,例如QRhiBuffer 、QRhiGraphicsPipeline 及相关对象。
缓冲区中的数据会通过QRhiResourceUpdateBatch 和QRhiCommandBuffer::resourceUpdate()进行更新(更准确地说,是将数据更新操作加入队列)。顶点缓冲区在初始顶点集上传完毕后,其内容便不再改变。 然而,统一缓冲区是一个dynamic 缓冲区,这与此类缓冲区的典型特性一致。其内容(至少是某些区域)会在每一帧中更新。因此,会无条件地调用updateDynamicBuffer(),偏移量为0,字节大小为4(即sizeof(float) ,因为C++中的float 类型恰好与GLSL的32位float 类型匹配)。 该位置存储的是t 的值,该值在每一帧(即每次调用frameStart()时)都会被更新。
缓冲区中还有另一个浮点数值,起始偏移量为 4。该值用于处理 3D API 之间的坐标系差异: 当isYUpInNDC()返回false 时(特别是Vulkan的情况),该值会被设置为-1.0,这会导致传递给片段着色器(经过插值处理)的2维向量中的Y值被翻转,而颜色正是基于该向量计算的。 这样一来,无论使用哪种3D API,屏幕上的输出效果都完全一致(即左上角呈绿色调,左下角呈红色调)。 该值在统一缓冲区中仅更新一次,这与顶点缓冲区类似。这凸显了旨在实现可移植性的低级渲染代码经常需要处理的一个问题:归一化设备坐标(NDC)与图像及帧缓冲区中的坐标系差异。 例如,除 Vulkan 之外,NDC 普遍采用左下角为原点的坐标系;而除 OpenGL 之外,帧缓冲区普遍采用左上角为原点的坐标系。 典型的采用透视投影的渲染器通常可以忽略此问题,只需便捷地调用 `QRhi::clipSpaceCorrMatrix()` 即可。该函数返回的矩阵可与投影矩阵相乘,既能在需要时应用 Y 轴翻转,又能适应以下事实:在 OpenGL 中,剪裁空间深度方向为 `-1..1 `,而在其他所有情况下均为 `0..1 `。 然而,在某些情况下(例如本例中),此方法并不适用。相反,应用程序和着色器逻辑需要根据对QRhi::isYUpInNDC() 和QRhi::isYUpInFramebuffer() 的查询结果,酌情对顶点和 UV 位置进行必要的调整。
要访问Qt Quick 所使用的QRhi 和QRhiSwapChain 对象,只需从QQuickWindow 中查询即可。请注意,这假设QQuickWindow 是一个常规的屏幕窗口。如果它使用的是QQuickRenderControl (例如,用于将渲染结果渲染到纹理中),则查询swapchain将是不正确的,因为此时并不存在swapchain。
由于该信号是在Qt Quick 调用QRhi::beginFrame()之后发出的,因此此时已经可以从交换链中查询命令缓冲区和渲染目标。正是这一点使得我们可以方便地在QRhiSwapChain::currentFrameCommandBuffer()返回的对象上调用QRhiCommandBuffer::resourceUpdate()。在创建图形管道时,可以从QRhiSwapChain::currentFrameRenderTarget()返回的QRhiRenderTarget 中获取QRhiRenderPassDescriptor 。 (请注意,这意味着此处构建的图形管道仅适用于渲染到交换链(swapchain),或者至多适用于与其compatible 的另一个渲染目标;如果想要渲染到纹理,则很可能需要不同的QRhiRenderPassDescriptor ,从而需要不同的图形管道,因为纹理和交换链的格式可能不同)
voidSquircleRenderer::frameStart()
{
// 如果有渲染线程,则在此线程上调用此函数。
QRhi*rhi = m_window->rhi();
if(!rhi) {
qWarning("QQuickWindow is not using QRhi for rendering");
return;
}
QRhiSwapChain*swapChain = m_window->swapChain();
if(!swapChain) {
qWarning("No QRhiSwapChain?");
return;
}
QRhiResourceUpdateBatch*resourceUpdates = rhi->nextResourceUpdateBatch();
if(!m_pipeline) {
m_vertexShader=getShader(QLatin1String(":/scenegraph/rhiunderqml/squircle_rhi.vert.qsb"));
if(!m_vertexShader.isValid())
qWarning("Failed to load vertex shader; rendering will be incorrect");
m_fragmentShader=getShader(QLatin1String(":/scenegraph/rhiunderqml/squircle_rhi.frag.qsb"));
if(!m_fragmentShader.isValid())
qWarning("Failed to load fragment shader; rendering will be incorrect");
m_vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable,QRhiBuffer::VertexBuffer, sizeof(vertices)));
m_vertexBuffer->create();
resourceUpdates->uploadStaticBuffer(m_vertexBuffer.get(),vertices);
constquint32 UBUF_SIZE= 4 + 4;// 2 个浮点数
m_uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,QRhiBuffer::UniformBuffer,UBUF_SIZE));
m_uniformBuffer->create();
floatyDir= rhi->isYUpInNDC()? 1.0f:-1.0f;
resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 4, 4, &yDir);
m_srb.reset(rhi->newShaderResourceBindings());
const autovisibleToAll=QRhiShaderResourceBinding::VertexStage|QRhiShaderResourceBinding::FragmentStage;
m_srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0,visibleToAll,m_uniformBuffer.get())
});
m_srb->create();
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({
{2 * sizeof(float) }
});
inputLayout.setAttributes({
{0, 0,QRhiVertexInputAttribute::Float2, 0}
});
m_pipeline.reset(rhi->newGraphicsPipeline());
m_pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QRhiGraphicsPipeline::TargetBlendblend;
blend.enable= true;
blend.srcColor=QRhiGraphicsPipeline::SrcAlpha;
blend.srcAlpha=QRhiGraphicsPipeline::SrcAlpha;
blend.dstColor=QRhiGraphicsPipeline::One;
blend.dstAlpha=QRhiGraphicsPipeline::One;
m_pipeline->setTargetBlends({ blend });
m_pipeline->setShaderStages({
{ QRhiShaderStage::Vertex,m_vertexShader },
{ QRhiShaderStage::Fragment,m_fragmentShader }
});
m_pipeline->setVertexInputLayout(inputLayout);
m_pipeline->setShaderResourceBindings(m_srb.get());
m_pipeline->setRenderPassDescriptor(swapChain->currentFrameRenderTarget()->renderPassDescriptor());
m_pipeline->create();
}
floatt=m_t;
resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 0, 4, &t);
swapChain->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates);
}最后,在调用QQuickWindow::beforeRenderPassRecording() 时,会记录一个包含 4 个顶点的三角带绘制调用。实际上,本示例仅绘制了一个四边形,并通过片段着色器中的逻辑计算像素颜色,但应用程序可以自由进行更复杂的绘制:创建多个图形管道并记录多个绘制调用也是完全可行的。 需要牢记的一点是:无论在从窗口的 `swapchain` 获取的 `QRhiCommandBuffer ` 上记录了什么内容,它都会在主渲染阶段中,被有效地插入到 `Qt Quick ` 场景图自身的渲染之前。
注意:这意味着如果 涉及深度缓冲区的使用(包括深度测试和写入深度值),则Qt Quick 的内容可能会受到写入深度缓冲区的值的影响。有关场景图渲染器的详细信息,特别是关于不透明 和Alpha混合基元处理的部分,请参阅《Qt Quick 场景图默认渲染器》。
要获取窗口的像素尺寸,可使用QRhiRenderTarget::pixelSize()。这样做很方便,因为这样示例就不需要通过其他方式计算视口大小,也不必担心是否需要应用high DPI scale factor (如果存在的话)。
void SquircleRenderer::mainPassRecordingStart()
{
// This function is invoked on the render thread, if there is one.
QRhi *rhi = m_window->rhi();
QRhiSwapChain *swapChain = m_window->swapChain();
if (!rhi || !swapChain)
return;
const QSize outputPixelSize = swapChain->currentFrameRenderTarget()->pixelSize();
QRhiCommandBuffer *cb = m_window->swapChain()->currentFrameCommandBuffer();
cb->setViewport({ 0.0f, 0.0f, float(outputPixelSize.width()), float(outputPixelSize.height()) });
cb->setGraphicsPipeline(m_pipeline.get());
cb->setShaderResources();
const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuffer.get(), 0);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(4);
}顶点着色器和片段着色器均通过标准的QRhi 着色器处理管道。它们最初以兼容Vulkan的GLSL语言编写,先编译为SPIR-V,随后由Qt工具转译为其他着色语言。 在使用 CMake 时,该示例依赖于 `qt_add_shaders ` 命令,该命令可简单便捷地将着色器与应用程序打包,并在构建时执行必要的处理。详情请参阅`qt_add_shaders()`。
指定BASE 有助于去除../shared 前缀,而PREFIX 则会添加预期的/scenegraph/rhiunderqml 前缀。因此最终路径为:/scenegraph/rhiunderqml/squircle_rhi.vert.qsb 。
qt_add_shaders(rhiunderqml "rhiunderqml_shaders"
PRECOMPILE
OPTIMIZED
PREFIX
/scenegraph/rhiunderqml
BASE
../shared
FILES
../shared/squircle_rhi.vert
../shared/squircle_rhi.frag
)为了支持 qmake,该示例仍包含通常在构建时生成的.qsb 文件,并在 qrc 文件中列出了这些文件。不过,对于使用 CMake 作为构建系统的新应用程序,不建议采用这种方法。
© 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.