立方体 RHI 小工具示例
展示如何使用QRhi Qt 3D API 和着色语言抽象层渲染纹理立方体并与QPainter 和小工具集成。
立方体 RHI Widget 示例截图
本示例以简单 RHI Widget 示例为基础。虽然该简单示例有意将其最小化并尽可能紧凑,只渲染了一个三角形,窗口中没有任何其他部件,但本应用程序却展示了这一点:
- 在窗口中拥有各种部件,其中一些部件控制着QRhiWidget 子类消耗的数据。
- 这里的QRhiWidget 并没有持续请求更新,而是在某些相关数据发生变化时才更新其背景纹理中的内容。
- 立方体的纹理由QRhiTexture 制作,该纹理的内容来自QImage ,该 包含通过QPainter 执行的基于软件的渲染。
- QRhiWidget can be read back 的内容并保存为图像文件(如 PNG 文件)。
- 运行时,4 倍多采样抗锯齿can be toggled 。QRhiWidget 子类准备正确处理不断变化的采样计数。
- 可通过滑块在 16x16 至 512x512 像素之间动态切换和控制强制指定的背景纹理尺寸。
- QRhiWidget 子类能正确处理不断变化的QRhi 。这可以在将窗口小部件设置为顶层(无父窗口;成为一个单独的窗口),然后再将其重新设置为主窗口的子窗口层次结构时看到。
- 最重要的是,有些部件甚至可以半透明地放置在QRhiWidget 的顶部,这证明正确的堆叠和混合是可行的。在这种情况下,QRhiWidget 要优于嵌入本地窗口,即使用QWidget::createWindowContainer() 基于QRhi 的QWindow ,因为它允许堆叠和剪切的方式与任何普通的、软件渲染的QWidget 相同,而嵌入本地窗口可能会因平台不同而受到各种限制,例如,在顶部放置附加控件往往会很困难或效率低下。
在重新实现initialize() 时,首先要检查我们上次使用的QRhi 是否仍然是最新的,以及采样计数(用于多采样抗锯齿)是否发生了变化。前者很重要,因为当QRhi 发生变化时,必须释放所有图形资源,而对于动态变化的采样计数,则会出现类似的问题,特别是对QRhiGraphicsPipeline 对象而言,因为这些对象中包含了采样计数。为简单起见,应用程序会以同样的方式处理所有此类变化,即重置scene
结构为默认构建的结构,从而方便地释放所有图形资源。然后重新创建所有资源。
当背景纹理尺寸(即渲染目标尺寸)发生变化时,不需要进行特殊处理,但为了方便,main() 会发出一个信号,以便重新定位叠加标签。每当QRhi 发生变化时,也会通过查询QRhi::backendName() 信号显示 3D API 名称。
执行时必须注意,多采样抗锯齿意味着colorTexture() 是nullptr
,而msaaColorBuffer() 是有效的。这与不使用 MSAA 时的情况正好相反。区分并使用不同类型(QRhiTexture,QRhiRenderBuffer )的原因是,在不支持多采样纹理但支持多采样渲染缓冲区的 3D 图形应用程序接口中使用 MSAA。OpenGL ES 3.0 就是一个例子。
在检查最新像素大小和采样计数时,一个方便、简洁的解决方案是通过QRhiRenderTarget 查询,因为这样就无需检查 colorTexture() 和 msaaColorBuffer() 中的哪个有效。
void ExampleRhiWidget::initialize(QRhiCommandBuffer *) { if (m_rhi != rhi()) { m_rhi = rhi(); scene = {}; emit rhiChanged(QString::fromUtf8(m_rhi->backendName())); } if (m_pixelSize != renderTarget()->pixelSize()) { m_pixelSize = renderTarget()->pixelSize(); emit resized(); } if (m_sampleCount != renderTarget()->sampleCount()) { m_sampleCount = renderTarget()->sampleCount(); scene = {}; }
其余部分不言自明。必要时,缓冲区和管道会(重新)创建。更新用于为立方体网格贴图的纹理内容。使用透视投影渲染场景。目前视图只是一个简单的转换。
if (!scene.vbuf) { initScene(); updateCubeTexture(); } scene.mvp = m_rhi->clipSpaceCorrMatrix(); scene.mvp.perspective(45.0f, m_pixelSize.width() / (float) m_pixelSize.height(), 0.01f, 1000.0f); scene.mvp.translate(0, 0, -4); updateMvp(); }
执行均匀缓冲区写入实际排队的函数也会考虑用户提供的旋转,从而生成最终的模型视图-投影矩阵。
void ExampleRhiWidget::updateMvp() { QMatrix4x4 mvp = scene.mvp * QMatrix4x4(QQuaternion::fromEulerAngles(QVector3D(30, itemData.cubeRotation, 0)).toRotationMatrix()); if (!scene.resourceUpdates) scene.resourceUpdates = m_rhi->nextResourceUpdateBatch(); scene.resourceUpdates->updateDynamicBuffer(scene.ubuf.get(), 0, 64, mvp.constData()); }
在渲染立方体时,更新片段着色器中采样的QRhiTexture 非常简单,尽管其中发生了很多事情:首先在QImage 中生成基于QPainter 的绘图。该绘图使用用户提供的文本。然后,CPU 端的像素数据被上传到纹理中(更准确地说,上传操作被记录在QRhiResourceUpdateBatch 中,随后在 render() 中提交)。
void ExampleRhiWidget::updateCubeTexture() { QImage image(CUBE_TEX_SIZE, QImage::Format_RGBA8888); const QRect r(QPoint(0, 0), CUBE_TEX_SIZE); QPainter p(&image); p.fillRect(r, QGradient::DeepBlue); QFont font; font.setPointSize(24); p.setFont(font); p.drawText(r, itemData.cubeText); p.end(); if (!scene.resourceUpdates) scene.resourceUpdates = m_rhi->nextResourceUpdateBatch(); scene.resourceUpdates->uploadTexture(scene.cubeTex.get(), image); }
图形资源初始化非常简单。只有一个顶点缓冲区,没有索引缓冲区和一个统一缓冲区,其中只有一个 4x4 矩阵(16 个浮点)。
包含QPainter 生成的图形的纹理大小为 512x512。请注意,在使用QRhi 时,所有尺寸(纹理尺寸、视口、剪刀、纹理上传区域等)的单位都是像素。要在着色器中对纹理进行采样,需要使用sampler object (尽管基于QRhi 的应用程序通常会在 GLSL 着色器代码中使用组合图像采样器,然后可能会被某些着色语言转换为单独的纹理和采样器对象,或被其他语言转换为组合纹理-采样器对象,这意味着在运行时,根据 3D API 的不同,引擎盖下可能实际上并不存在本地采样器对象,但这对应用程序来说都是透明的)。
顶点着色器从绑定点 0 的均匀缓冲区读取数据,因此scene.ubuf
会在该绑定位置显示。片段着色器对绑定点 1 提供的纹理进行采样,因此为该绑定位置指定了一对组合纹理-采样器。
QRhiGraphicsPipeline 可进行深度测试/写入,并清除反面。它还依赖于一些默认设置,例如深度比较功能默认为Less
,这对我们来说没有问题,正面模式为逆时针,这也是很好的设置,因此无需再次设置。
scene.vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube))); scene.vbuf->create(); scene.resourceUpdates = m_rhi->nextResourceUpdateBatch(); scene.resourceUpdates->uploadStaticBuffer(scene.vbuf.get(), cube); scene.ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64)); scene.ubuf->create(); scene.cubeTex.reset(m_rhi->newTexture(QRhiTexture::RGBA8, CUBE_TEX_SIZE)); scene.cubeTex->create(); scene.sampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge)); scene.sampler->create(); scene.srb.reset(m_rhi->newShaderResourceBindings()); scene.srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, scene.ubuf.get()), QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, scene.cubeTex.get(), scene.sampler.get()) }); scene.srb->create(); scene.ps.reset(m_rhi->newGraphicsPipeline()); scene.ps->setDepthTest(true); scene.ps->setDepthWrite(true); scene.ps->setCullMode(QRhiGraphicsPipeline::Back); scene.ps->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/texture.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/texture.frag.qsb")) } }); QRhiVertexInputLayout inputLayout; // The cube is provided as non-interleaved sets of positions, UVs, normals. // Normals are not interesting here, only need the positions and UVs. inputLayout.setBindings({ { 3 * sizeof(float) }, { 2 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, { 1, 1, QRhiVertexInputAttribute::Float2, 0 } }); scene.ps->setSampleCount(m_sampleCount); scene.ps->setVertexInputLayout(inputLayout); scene.ps->setShaderResourceBindings(scene.srb.get()); scene.ps->setRenderPassDescriptor(renderTarget()->renderPassDescriptor()); scene.ps->create();
在重新实现render() 时,首先要检查用户提供的数据。如果控制旋转的QSlider 提供了新值,或者带有立方体文本的QTextEdit 更改了文本,那么依赖于这些数据的图形资源内容就会得到更新。
然后,将记录一次绘制调用的单次渲染过程。立方体网格数据是以非交错格式提供的,因此需要两个顶点输入绑定,一个是位置(x、y、z),另一个是 UV(u、v),起始偏移量对应 36 个 x-yz 浮点对。
void ExampleRhiWidget::render(QRhiCommandBuffer *cb) { if (itemData.cubeRotationDirty) { itemData.cubeRotationDirty = false; updateMvp(); } if (itemData.cubeTextDirty) { itemData.cubeTextDirty = false; updateCubeTexture(); } QRhiResourceUpdateBatch *resourceUpdates = scene.resourceUpdates; if (resourceUpdates) scene.resourceUpdates = nullptr; const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f); cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates); cb->setGraphicsPipeline(scene.ps.get()); cb->setViewport(QRhiViewport(0, 0, m_pixelSize.width(), m_pixelSize.height())); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBindings[] = { { scene.vbuf.get(), 0 }, { scene.vbuf.get(), quint32(36 * 3 * sizeof(float)) } }; cb->setVertexInput(0, 2, vbufBindings); cb->draw(36); cb->endPass(); }
如何发送用户提供的数据?以旋转为例。main() 连接到QSlider 的valueChanged 信号。信号发出后,连接的 lamda 会调用 ExampleRhiWidget 上的 setCubeRotation()。在这里,如果值与之前的不同,就会被存储并设置脏标志。然后,最重要的是在 ExampleRhiWidget 上调用update()。这将触发将新帧渲染到QRhiWidget 的背景纹理中。如果不这样做,拖动滑块时 ExampleRhiWidget 的内容就不会更新。
void setCubeTextureText(const QString &s) { if (itemData.cubeText == s) return; itemData.cubeText = s; itemData.cubeTextDirty = true; update(); } void setCubeRotation(float r) { if (itemData.cubeRotation == r) return; itemData.cubeRotation = r; itemData.cubeRotationDirty = true; update(); }
© 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.