Qt是目前最先进、最完整的跨平台C开发工具。它不仅完全实现了一次编写所有平台无差别运行更提供了几乎所有开发过程中需要用到的工具。如今Qt已被运用于超过70个行业、数千家企业支持数百万设备及应用。本文将为大家演示如何使用QRhi、Qt的3D API和着色语言抽象层渲染三角形。在很多方面这个示例都是QWidget世界中的RHI窗口示例的对应。这个应用程序中的QRhiWidget子类使用带有基本顶点和片段着色器的简单图形管道渲染单个三角形。与普通的基于QWindow的应用程序不同本示例不需要担心较低级别的细节比如设置窗口和QRhi或者处理交换链和窗口事件因为这些都由这里的QWidget框架负责。QRhiWidget子类的实例被添加到QVBoxLayout中为了使示例保持最小和紧凑没有引入更多的小部件或3D内容。在上文中我们为大家介绍了结构和main()本文将继续介绍如何完成渲染渲染设置在examplewidget.cpp中小部件实现使用一个辅助函数从.qsb文件加载一个QShader对象这个应用程序通过Qt资源系统将预置的.qsb文件嵌入到可执行文件中。由于模块依赖(并且由于仍然支持qmake)本例不使用方便的CMake函数qt_add_shaders()而是随.qsb文件一起作为源代码树的一部分。我们鼓励现实世界的应用程序避免这种情况而是使用Qt Shader 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()总是给出最新的、最新的像素大小。这个例子没有准备好改变纹理格式和多样本设置因为它只使用默认值(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); }最后计算投影矩阵。这取决于小部件的大小因此在每次函数调用中都无条件地完成。注意投影矩阵包括来自QRhi的校正矩阵以适应归一化设备坐标的3D API差异。(例如Y向下 vs. 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对象并不罕见只要该对象与管道创建时给出的布局兼容即可。没有索引缓冲区只有一个顶点缓冲区绑定(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(); }