立方体 RHI 控件示例

演示如何使用 Qt 3D API 及着色语言抽象层(QRhi ),渲染带纹理的立方体,并将其与QPainter 及 Qt Widgets 集成。

展示复杂渲染叠加层的应用程序

立方体 RHI 控件示例的屏幕截图

本示例基于“简单 RHI 控件示例”构建。虽然简单示例刻意保持极简且尽可能精炼——仅渲染一个三角形且窗口中不包含其他控件——但本应用程序演示了:

  • 窗口中包含多种控件,其中部分控件控制的数据会被 `QRhiWidget ` 子类所使用。
  • 此处的QRhiWidget 不会持续请求更新,而是在相关数据发生变化时才更新其底层纹理中的内容。
  • 该立方体的纹理渲染使用了一个QRhiTexture ,其内容源自QImage ,该对象包含通过QPainter 实现的软件渲染。
  • 该QRhiWidget 的can be read back 内容会被保存到图像文件中(例如PNG文件)。
  • 运行时支持 4 倍多采样抗锯齿的can be toggled 。QRhiWidget 子类已做好准备,能够正确处理不断变化的采样计数。
  • explicitly specified backing texture size 的强制启用状态可动态切换,并通过滑块在16x16至512x512像素之间进行调节。
  • QRhiWidget 子类能够正确处理不断变化的QRhi 。当将该控件设为顶级控件(无父控件;成为独立窗口),然后将其重新添加为主窗口的子控件时,可以观察到这一行为。
  • 最重要的是,某些小部件(甚至具有半透明效果的)可以放置在QRhiWidget 之上,这证明了正确的堆叠和混合是可行的。这是QRhiWidget 优于嵌入原生窗口(即基于QRhi 的QWindow ,使用QWidget::createWindowContainer())的一个实例, 因为它能够像任何普通的、软件渲染的QWidget 一样进行堆叠和裁剪,而原生窗口嵌入则可能因平台不同而存在各种限制,例如,在顶部放置额外控件往往比较困难或效率低下。

在initialize()的重写中,首先需要检查上次处理的QRhi 是否仍然有效,以及采样数(用于多采样抗锯齿)是否发生了变化。 前者很重要,因为当QRhi 发生变化时,所有图形资源都必须被释放;而对于动态变化的采样数,QRhiGraphicsPipeline 对象会引发类似的问题,因为这些对象会将采样数“固化”其中。为简化起见,应用程序以相同的方式处理所有此类变化,即将其scene 结构重置为默认构造的实例,这会方便地释放所有图形资源。 随后将重新创建所有资源。

当底层纹理大小(即渲染目标大小)发生变化时,无需采取特殊操作,但为了方便起见会发出一个信号,以便 main() 函数能够重新定位叠加标签。每当QRhi 发生变化时,也会通过查询QRhi::backendName() 来通过信号暴露 3D API 的名称。

实现时必须注意:多采样抗锯齿(MSAA)意味着colorTexture()的返回值为nullptr ,而msaaColorBuffer()的返回值则为有效值。这与未启用MSAA时的情况恰好相反。 之所以进行区分并使用不同类型(QRhiTexture 、QRhiRenderBuffer ),是为了允许在不支持多采样纹理但支持多采样渲染缓冲区的 3D 图形 API 中使用 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-y-z 浮点数。

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 信号。当该信号发出时,连接的lambda表达式会在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();
    }

示例项目 @ code.qt.io

另请参阅 QRhi 、简单 RHI 控件示例 以及RHI 窗口示例。

© 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.