场景图 - QML 下的 RHI
展示如何在Qt Quick 场景下直接使用QRhi 渲染。
简介
QML 下的 RHI 示例展示了应用程序如何利用QQuickWindow::beforeRendering() 和QQuickWindow::beforeRenderPassRecording() 信号在Qt Quick 场景下绘制基于QRhi 的自定义内容。
对于希望在Qt Quick 场景上呈现QRhi 内容的应用程序,可使用QQuickWindow::beforeRendering() 将数据上传到缓冲区,并连接到QQuickWindow::afterRenderPassRecording() 信号。
在此示例中,我们还将看到如何让暴露给 QML 的值影响基于QRhi 的渲染。我们在 QML 文件中使用NumberAnimation 对阈值进行动画处理,然后将浮点值通过统一缓冲区传递给片段着色器。
该示例在大多数方面与OpenGL Under QML、Direct3D 11 Under QML、Metal Under QML 和Vulkan Under QML示例相当。这些示例通过直接使用 3D API 来呈现相同的内容。而本示例则完全跨平台、可移植,因为它本质上支持使用QRhi 支持的所有 3D API(如 OpenGL、Vulkan、Metal、Direct 3D 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 场景图。
除了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 。(注意,这意味着此处构建的图形管道仅适用于渲染到交换链,或最多渲染到与交换链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.VertexBuffer, sizeof(vertices)) QRhiBuffer::VertexBuffer, sizeof(vertices))); m_vertexBuffer->create(); resourceUpdates->uploadStaticBuffer(m_vertexBuffer.get(),vertices);constquint32UBUF_SIZE= 4 + 4;// 2 个浮点m_uniformBuffer.reset(rhi->newBuffer(QRhiBufferm_uniformBuffer.reset(rhi->newBuffer( 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: :顶点阶段 QRhiShaderResourceBinding::碎片阶段; m_srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0,visibleToAll,m_uniformBuffer.get()) }); m_srb->create(); QRhiVertexInputLayoutinputLayout; inputLayout.setBindings({ {2 * sizeof(float) } }); inputLayout.setAttributes({ {0, 0、 QRhiVertexInputAttribute::Float2, 0} }); m_pipeline.reset(rhi->newGraphicsPipeline()); m_pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip); QRhiGraphicsPipeline::TargetBlend blend; blend.enable= true; blend.srcColor=QRhiGraphicsPipeline::SrcAlpha;blend.srcAlpha= ::SrcAlpha。QRhiGraphicsPipeline::SrcAlpha; blend.dstColor=QRhiGraphicsPipeline::一; blend.dstAlpha=QRhiGraphicsPipeline::One; m_pipeline->setTargetBlends({ blend }); m_pipeline->setShaderStages({ { QRhiShaderStage顶点,m_vertexShader },{ QRhiShaderStage;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 的内容可能会受到写入深度缓冲区的值的影响。有关场景图渲染器的详细信息,请参阅Qt Quick 场景图默认渲染器,特别是有关处理不透明和alpha 混合基元的部分。
要获得以像素为单位的窗口大小,需要使用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 XMLShader Tools Build System Integration。
指定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 作为编译系统的新应用程序,我们不建议使用这种方法。
© 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.