Qt 是目前最先进、最完整的跨平台C++开发工具。它不仅完全实现了一次编写,所有平台无差别运行,更提供了几乎所有开发过程中需要用到的工具。如今,Qt已被运用于超过70个行业、数千家企业,支持数百万设备及应用。
本文将为大家演示如何使用QRhi、Qt的3D API和着色语言抽象层渲染三角形。
点击获取Qt Widget组件下载(Q技术交流:166830288)
在很多方面,这个示例都是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();
}
Qt Widget组件推荐
- QtitanRibbon - Ribbon UI组件:是一款遵循Microsoft Ribbon UI Paradigm for Qt技术的Ribbon UI组件,QtitanRibbon致力于为Windows、Linux和Mac OS X提供功能完整的Ribbon组件。
- QtitanChart - Qt类图表组件:是一个C ++库,代表一组控件,这些控件使您可以快速地为应用程序提供漂亮而丰富的图表。
- QtitanDataGrid - Qt网格组件:提供了一套完整的标准 QTableView 函数和传统组件无法实现的独特功能。使您能够将不同来源的各类数据加载到一个快速、灵活且功能强大的可编辑网格中,支持排序、分组、报告、创建带状列、拖放按钮和许多其他方便的功能。
- QtitanDocking:允许您像 Visual Studio 一样为您的伟大应用程序配备可停靠面板和可停靠工具栏。黑色、白色、蓝色调色板完全支持 Visual Studio 2019 主题!