RHI 窗口示例

本示例展示了如何使用 QRhi 创建一个最基础的 QWindow 应用程序。

Qt 6.6 开始提供其加速 3D API 和着色器抽象层,以供应用程序使用。应用程序现在可以使用 Qt 本身用来实现 Qt Quick 场景图或 Qt Quick 3D 引擎的相同 3D 图形类。在早期版本的 Qt 中,QRhi 及相关类都是私有 API。从 6.6 版本开始,这些类与 QPA 类族相似,既不完全公开也不完全私有,介于两者之间,与公开 API 相比,兼容性承诺更为有限。另一方面,QRhi 及相关类现在拥有与公共 API 类似的完整文档。

使用 QRhi 的方法多种多样,本例展示了最底层的方法:针对 QWindow,而不使用任何形式上的 Qt Quick、Qt Quick 3D 或 Widgets,并在应用程序中设置所有的渲染和窗口基础结构。

相比之下,当你使用 Qt Quick 或 Qt Quick 3D 编写 QML 应用程序,并希望向其中添加 QRhi 基础的渲染时,这样的应用程序将依赖 Qt Quick 已经初始化的窗口和渲染基础设施,很可能还需要从 QQuickWindow 获取一个现有的 QRhi 实例。在那里处理 QRhi::create(),平台/ API 特殊性如 Vulkan 实例,或正确处理窗口的 expose 和 resize 事件,都由 Qt Quick 管理。而在这个示例中,所有这些都由应用程序本身管理。

注意:对于 QWidget-based 应用程序尤其如此,应注意 QWidget::createWindowContainer() 允许将 QWindow (由本地窗口支持) 嵌入基于 Widgets 的用户界面。因此,本例中的 HelloWindow 类可以在 QWidget-based 应用程序中重用,前提是已经从 main() 函数中进行了必要的初始化。

3D API 支持

应用程序支持所有当前 QRhi 后端。如果没有指定命令行参数,则使用平台特定的默认值:Windows 上的 Direct3D 11,Linux 上的 OpenGL,macOS/iOS 上的 Metal。

使用 --help 运行会显示可用的命令行选项

  • -d 或 –d3d11 用于 Direct3D 11
  • -D 或 –d3d12 用于 Direct3D 12
  • -m 或 –metal 用于 Metal
  • -v 或 –vulkan 用于 Vulkan
  • -g 或 –opengl 用于 OpenGL 或 OpenGL ES
  • -n 或 –null 用于 Null 后端

构建系统注意事项

此应用程序仅依赖于 Qt GUI 模块。它不使用 Qt Widgets 或 Qt Quick。

为了访问RHI API,这是所有Qt应用程序都可以使用,但具有有限兼容性承诺的API,CMake命令target_link_libraries列出了Qt6::GuiPrivate。这就是为什么}#include <rhi/qrhi.h>包含声明可以成功编译。

功能

应用程序功能

  • 可调整大小的QWindow
  • 一个替换链和深度-模板缓冲区,它正确地跟踪窗口的大小,
  • 基于诸如QExposeEventQPlatformSurfaceEvent等事件进行初始化、渲染和拆除的逻辑,
  • 使用通过QPainter(使用栅格化绘图引擎,即图像像素数据的生成全部基于CPU,然后这些数据被上传到GPU纹理)在QImage中生成的纹理渲染全屏纹理四边形,
  • 启用混合和深度测试的三角形渲染,使用透视投影,同时应用每帧变化的模型变换,
  • 使用requestUpdate()的跨平台高效渲染循环。

着色器

应用程序使用两套顶点和片元着色器对

  • 一个用于全屏四边形,顶点输入为空,片元着色器采样纹理(quad.vertquad.frag),
  • 另一个用于三角形,顶点位置和颜色在顶点缓冲区中提供,视图-投影矩阵在统一缓冲区中提供(color.vertcolor.frag)。

着色器编写为Vulkan兼容的GLSL源代码。

由于是Qt GUI模块示例,此示例不能依赖Qt Shader Tools模块。这意味着CMake辅助函数如qt_add_shaders不可用。因此,示例在shaders/prebuilt文件夹中包含了预处理的.qsb文件,并且它们通过qt_add_resources直接包含在可执行文件中。这种方法不一定适用于应用程序,更建议使用qt_add_shaders,以避免手动生成和管理.qsb文件。

为此示例生成.qsb文件,使用了命令qsb --qt6 color.vert -o prebuilt/color.vert.qsb等。这导致编译到SPIR-V,然后转换为GLSL(100 es120),HLSL(5.0),和MSL(1.2)。然后将所有着色器版本打包在一起成为一个QShader并将它们序列化到磁盘上。

特定API的初始化

对于一些3D API,main()函数必须执行适当的API特定初始化,例如在使用Vulkan时创建一个QVulkanInstance。对于OpenGL,我们必须确保深度缓冲区可用,这通过QSurfaceFormat来完成。这些步骤不属于QRhi的范围,因为QRhi的OpenGL或Vulkan后端建立在现有Qt设施(如QOpenGLContextQVulkanInstance)的基础上。

   // For OpenGL, to ensure there is a depth/stencil buffer for the window.
   // With other APIs this is under the application's control (QRhiRenderBuffer etc.)
   // and so no special setup is needed for those.
   QSurfaceFormat fmt;
   fmt.setDepthBufferSize(24);
   fmt.setStencilBufferSize(8);
   // Special case macOS to allow using OpenGL there.
   // (the default Metal is the recommended approach, though)
   // gl_VertexID is a GLSL 130 feature, and so the default OpenGL 2.1 context
   // we get on macOS is not sufficient.
#ifdef Q_OS_MACOS
   fmt.setVersion(4, 1);
   fmt.setProfile(QSurfaceFormat::CoreProfile);
#endif
   QSurfaceFormat::setDefaultFormat(fmt);

   // For Vulkan.
#if QT_CONFIG(vulkan)
   QVulkanInstance inst;
   if (graphicsApi == QRhi::Vulkan) {
       // Request validation, if available. This is completely optional
       // and has a performance impact, and should be avoided in production use.
       inst.setLayers({ "VK_LAYER_KHRONOS_validation" });
       // Play nice with QRhi.
       inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
       if (!inst.create()) {
           qWarning("Failed to create Vulkan instance, switching to OpenGL");
           graphicsApi = QRhi::OpenGLES2;
       }
   }
#endif

注意:对于Vulkan,请注意QRhiVulkanInitParams::preferredInstanceExtensions()是如何被考虑进去的,以确保启用适当的扩展。

HelloWindowRhiWindow 的子类,而 RhiWindow 又是 QWindow 的子类。 RhiWindow 包含了管理可调整大小的窗口及其双缓冲链(以及深度-模板缓冲区)所需的全部内容,因此它还可以在其他应用中被重用。 HelloWindow 包含了特定于这个示例应用的渲染逻辑。

QWindow 子类构造函数中,根据自己的选择的 3D API 来设置表面类型。

RhiWindow::RhiWindow(QRhi::Implementation graphicsApi)
    : m_graphicsApi(graphicsApi)
{
    switch (graphicsApi) {
    case QRhi::OpenGLES2:
        setSurfaceType(OpenGLSurface);
        break;
    case QRhi::Vulkan:
        setSurfaceType(VulkanSurface);
        break;
    case QRhi::D3D11:
    case QRhi::D3D12:
        setSurfaceType(Direct3DSurface);
        break;
    case QRhi::Metal:
        setSurfaceType(MetalSurface);
        break;
    case QRhi::Null:
        break; // RasterSurface
    }
}

RhiWindow::init() 中实现了创建并初始化 QRhi 对象的过程。请注意,当窗口是 renderable 时,这个函数会被调用,这是通过 曝光事件 来表示的。

根据我们所使用的 3D API,需要将适当的 InitParams 结构传递给 QRhi::create()。例如,使用 OpenGL 时,由应用程序创建并提供给 QRhi 的一个 QOffscreenSurface(或其他 QSurface)。使用 Vulkan 时,需要成功初始化的 QVulkanInstance。对于 Direct 3D 或 Metal 等其他情况,则不需要额外的信息即可初始化。

void RhiWindow::init()
{
    if (m_graphicsApi == QRhi::Null) {
        QRhiNullInitParams params;
        m_rhi.reset(QRhi::create(QRhi::Null, &params));
    }

#if QT_CONFIG(opengl)
    if (m_graphicsApi == QRhi::OpenGLES2) {
        m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface());
        QRhiGles2InitParams params;
        params.fallbackSurface = m_fallbackSurface.get();
        params.window = this;
        m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params));
    }
#endif

#if QT_CONFIG(vulkan)
    if (m_graphicsApi == QRhi::Vulkan) {
        QRhiVulkanInitParams params;
        params.inst = vulkanInstance();
        params.window = this;
        m_rhi.reset(QRhi::create(QRhi::Vulkan, &params));
    }
#endif

#ifdef Q_OS_WIN
    if (m_graphicsApi == QRhi::D3D11) {
        QRhiD3D11InitParams params;
        // Enable the debug layer, if available. This is optional
        // and should be avoided in production builds.
        params.enableDebugLayer = true;
        m_rhi.reset(QRhi::create(QRhi::D3D11, &params));
    } else if (m_graphicsApi == QRhi::D3D12) {
        QRhiD3D12InitParams params;
        // Enable the debug layer, if available. This is optional
        // and should be avoided in production builds.
        params.enableDebugLayer = true;
        m_rhi.reset(QRhi::create(QRhi::D3D12, &params));
    }
#endif

#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
    if (m_graphicsApi == QRhi::Metal) {
        QRhiMetalInitParams params;
        m_rhi.reset(QRhi::create(QRhi::Metal, &params));
    }
#endif

    if (!m_rhi)
        qFatal("Failed to create RHI backend");

除此之外,所有其他内容,包括所有渲染代码,都是完全跨平台的,没有任何针对特定的 3D API 的分支或条件。

曝光事件

renderable 的确切含义是平台特定的。例如,在 macOS 上,完全被其他窗口遮挡的窗口不是可渲染的,而在 Windows 上遮挡没有意义。幸运的是,应用不需要对此有特殊了解:Qt 的平台插件隐藏了曝光事件背后的差异。然而,在重写 exposeEvent() 时,也需要知道空输出大小(例如宽度高度为 0)也应该是被视为不可渲染的情况。例如,在 Windows 上,当最小化窗口时就会发生这种情况。因此,根据 QRhiSwapChain::surfacePixelSize 执行检查。

此曝光事件处理实现旨在提供鲁棒性、安全性和可移植性。Qt Quick 本身也在其渲染循环中实现了非常相似的逻辑。

void RhiWindow::exposeEvent(QExposeEvent *)
{
    // initialize and start rendering when the window becomes usable for graphics purposes
    if (isExposed() && !m_initialized) {
        init();
        resizeSwapChain();
        m_initialized = true;
    }

    const QSize surfaceSize = m_hasSwapChain ? m_sc->surfacePixelSize() : QSize();

    // stop pushing frames when not exposed (or size is 0)
    if ((!isExposed() || (m_hasSwapChain && surfaceSize.isEmpty())) && m_initialized && !m_notExposed)
        m_notExposed = true;

    // Continue when exposed again and the surface has a valid size. Note that
    // surfaceSize can be (0, 0) even though size() reports a valid one, hence
    // trusting surfacePixelSize() and not QWindow.
    if (isExposed() && m_initialized && m_notExposed && !surfaceSize.isEmpty()) {
        m_notExposed = false;
        m_newlyExposed = true;
    }

    // always render a frame on exposeEvent() (when exposed) in order to update
    // immediately on window resize.
    if (isExposed() && !surfaceSize.isEmpty())
        render();
}

在 RhiWindow::render() 中,这是响应由 requestUpdate 生成并由 UpdateRequest 事件触发时调用的,以下检查是存在的,以防止在交换链初始化失败或窗口变为不可渲染时尝试渲染。

void RhiWindow::render()
{
    if (!m_hasSwapChain || m_notExposed)
        return;

交换链、深度-模板缓冲区和调整大小

要向 QWindow 渲染,需要一个 QRhiSwapChain。除了创建一个作为深度-模板缓冲区使用的 QRhiRenderBuffer 之外,因为应用展示了如何在图形管道中启用深度测试。使用一些传统的 3D API 时,管理窗口的深度/模板缓冲区是窗口系统接口 API (EGL、WGL、GLX 等)的一部分(这意味着深度/模板缓冲区隐式地与管理窗口表面一起管理),而使用现代 API 时,基于窗口的渲染目标管理深度-模板缓冲区与离屏渲染目标没有区别。《QRhi》抽象了这个概念,但仍需要表明 QRhiRenderBuffer 是与 QRhiSwapChain 一同使用的。

QRhiSwapChainQWindow 和深度/模板缓冲区相关联。

    std::unique_ptr<QRhiSwapChain> m_sc;
    std::unique_ptr<QRhiRenderBuffer> m_ds;
    std::unique_ptr<QRhiRenderPassDescriptor> m_rp;

    m_sc.reset(m_rhi->newSwapChain());
    m_ds.reset(m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil,
                                      QSize(), // no need to set the size here, due to UsedWithSwapChainOnly
                                      1,
                                      QRhiRenderBuffer::UsedWithSwapChainOnly));
    m_sc->setWindow(this);
    m_sc->setDepthStencil(m_ds.get());
    m_rp.reset(m_sc->newCompatibleRenderPassDescriptor());
    m_sc->setRenderPassDescriptor(m_rp.get());

当窗口大小发生变化时,交换链也需要相应调整大小。这在resizeSwapChain()函数中实现。

void RhiWindow::resizeSwapChain()
{
    m_hasSwapChain = m_sc->createOrResize(); // also handles m_ds

    const QSize outputSize = m_sc->currentPixelSize();
    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);
}

与其他QRhiResource子类不同,QRhiSwapChain在它的创建函数方面具有略微不同的语义。正如其名称createOrResize()所暗示的那样,当确定输出窗口大小可能与交换链上次初始化时不同步时,需要调用该函数。与其关联的QRhiRenderBuffer深度模板自动设置其大小,并且从交换链的createOrResize()中隐式调用其create()。

这同样是一个计算投影和视图矩阵的便利地方,因为我们的透视投影设置取决于输出宽高比。

注意:为了消除坐标系统差异,从QRhi查询并烤接到投影矩阵中的一个后端/特定API的“校正”矩阵。这使得应用程序能够使用具有左下角原点的OpenGL风格的顶点数据来工作。

在发现当前报告的大小不再与交换链上次初始化的大小相同时,resizeSwapChain()函数将从RhiWindow::render()中调用。

有关更多信息,请参阅QRhiSwapChain::currentPixelSize()和QRhiSwapChain::surfacePixelSize()。

高DPI支持是内置的:大小,正如其名称所示,始终以像素为单位,考虑到了特定于窗口的缩放因子。在QRhi(和3D API)级别上,没有高DPI缩放的概念,一切都是像素级别的。这意味着,一个尺寸为1280x720且设备像素比为2的QWindow是一个具有(像素)大小为2560x1440的渲染目标(交换链)。

    // If the window got resized or newly exposed, resize the swapchain. (the
    // newly-exposed case is not actually required by some platforms, but is
    // here for robustness and portability)
    //
    // This (exposeEvent + the logic here) is the only safe way to perform
    // resize handling. Note the usage of the RHI's surfacePixelSize(), and
    // never QWindow::size(). (the two may or may not be the same under the hood,
    // depending on the backend and platform)
    //
    if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) {
        resizeSwapChain();
        if (!m_hasSwapChain)
            return;
        m_newlyExposed = false;
    }

渲染循环

应用程序持续渲染,通过显示率(垂直同步)进行节流。这通过在当前记录的帧提交后从RhiWindow::render()中调用requestUpdate()来确保。

    m_rhi->endFrame(m_sc.get());

    // Always request the next frame via requestUpdate(). On some platforms this is backed
    // by a platform-specific solution, e.g. CVDisplayLink on macOS, which is potentially
    // more efficient than a timer, queued metacalls, etc.
    requestUpdate();
}

这最终会导致获取一个UpdateRequest事件。这在地实现的事件()中进行处理。

bool RhiWindow::event(QEvent *e)
{
    switch (e->type()) {
    case QEvent::UpdateRequest:
        render();
        break;

    case QEvent::PlatformSurface:
        // this is the proper time to tear down the swapchain (while the native window and surface are still around)
        if (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed)
            releaseSwapChain();
        break;

    default:
        break;
    }

    return QWindow::event(e);
}

资源和管道设置

应用程序记录单个渲染通道,该通道发出两个绘制调用,使用两个不同的图形管线。一个是“背景”,包含由QPainter生成的图像的纹理,然后单一点在上方渲染,启用混合。

与三角形一起使用的顶点缓冲区和统一缓冲区创建方法如下。统一缓冲区的大小为68字节,因为着色器指定了一个mat4和一个float成员在统一块中。请注意std140布局规则。在这个例子中,这并没有带来惊喜,因为跟随mat4float成员具有正确的对齐,且没有任何额外的填充,但在其他应用程序中,它可能变得相关,特别是当与类型如vec2vec3一起工作时。如有疑问,考虑检查QShaderDescription中的QShader,或者,更方便的是,使用-d参数在.qsb文件上运行qsb工具来检查以人类可读形式显示的元数据。打印的信息包括,但不仅限于,统一块成员的偏移量、大小以及每个统一块的总字节数。

void HelloWindow::customInit()
{
    m_initialUpdates = m_rhi->nextResourceUpdateBatch();

    m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
    m_vbuf->create();
    m_initialUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData);

    static const quint32 UBUF_SIZE = 68;
    m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE));
    m_ubuf->create();

顶点着色器和片段着色器都需要在绑定点0处使用一个统一缓冲区。这通过QRhiShaderResourceBindings对象来确保。然后使用着色器和一些附加信息设置图形管道。此示例还依赖于一些方便的默认值,例如,原语拓扑是三角形,但这默认值,所以没有明确设置。有关更多详细信息,请参阅QRhiGraphicsPipeline

除了指定拓扑和各种状态外,管道还必须与以下内容相关联

   m_colorTriSrb.reset(m_rhi->newShaderResourceBindings());
   static const QRhiShaderResourceBinding::StageFlags visibility =
           QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
   m_colorTriSrb->setBindings({
           QRhiShaderResourceBinding::uniformBuffer(0, visibility, m_ubuf.get())
   });
   m_colorTriSrb->create();

   m_colorPipeline.reset(m_rhi->newGraphicsPipeline());
   // Enable depth testing; not quite needed for a simple triangle, but we
   // have a depth-stencil buffer so why not.
   m_colorPipeline->setDepthTest(true);
   m_colorPipeline->setDepthWrite(true);
   // Blend factors default to One, OneOneMinusSrcAlpha, which is convenient.
   QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
   premulAlphaBlend.enable = true;
   m_colorPipeline->setTargetBlends({ premulAlphaBlend });
   m_colorPipeline->setShaderStages({
       { QRhiShaderStage::Vertex, getShader(QLatin1String(":/color.vert.qsb")) },
       { QRhiShaderStage::Fragment, getShader(QLatin1String(":/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_colorPipeline->setVertexInputLayout(inputLayout);
   m_colorPipeline->setShaderResourceBindings(m_colorTriSrb.get());
   m_colorPipeline->setRenderPassDescriptor(m_rp.get());
   m_colorPipeline->create();

getShader()是一个辅助函数,用于加载.qsb文件并从中反序列化QShader

static QShader getShader(const QString &name)
{
    QFile f(name);
    if (f.open(QIODevice::ReadOnly))
        return QShader::fromSerialized(f.readAll());

    return QShader();
}

color.vert着色器指定以下作为顶点输入

layout(location = 0) in vec4 position;
layout(location = 1) in vec3 color;

然而,C++代码提供顶点数据为2个浮点位置,3个浮点颜色交错。 (xyrgb每个顶点)这就是为什么步进是5 * sizeof(float),并且对于位置0和1的输入指定为Float2Float3。这是有效的,并且vec4位置中的zw将自动设置。

渲染

记录帧的开始是通过调用 QRhi::beginFrame() 来实现的,结束是通过调用 QRhi::endFrame() 来完成的。

    QRhi::FrameOpResult result = m_rhi->beginFrame(m_sc.get());
    if (result == QRhi::FrameOpSwapChainOutOfDate) {
        resizeSwapChain();
        if (!m_hasSwapChain)
            return;
        result = m_rhi->beginFrame(m_sc.get());
    }
    if (result != QRhi::FrameOpSuccess) {
        qWarning("beginFrame failed with %d, will retry", result);
        requestUpdate();
        return;
    }

    customRender();

一些资源(缓冲区、纹理)在应用程序中具有静态数据,意味着其内容从未改变。例如,顶点缓冲区的内容是在初始化步骤中提供的,之后不会改变。这些数据更新操作记录在 m_initialUpdates 中。当尚未完成时,该资源更新批次的命令被合并到每帧批次中。

void HelloWindow::customRender()
{
    QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();

    if (m_initialUpdates) {
        resourceUpdates->merge(m_initialUpdates);
        m_initialUpdates->release();
        m_initialUpdates = nullptr;
    }

由于每帧的视图投影矩阵和技术透明度的更改,因此需要对每帧资源更新批次进行更新是必要的。

    m_rotation += 1.0f;
    QMatrix4x4 modelViewProjection = m_viewProjection;
    modelViewProjection.rotate(m_rotation, 0, 1, 0);
    resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData());
    m_opacity += m_opacityDir * 0.005f;
    if (m_opacity < 0.0f || m_opacity > 1.0f) {
        m_opacityDir *= -1;
        m_opacity = qBound(0.0f, m_opacity, 1.0f);
    }
    resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 64, 4, &m_opacity);

要开始记录绘制操作,查询一个 QRhiCommandBuffer,并确定输出大小,这将在设置视口和调整所需的满屏纹理时非常有用。

    QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
    const QSize outputSizeInPixels = m_sc->currentPixelSize();

开始绘制操作意味着清除渲染目标的颜色和深度-模板缓冲区(除非渲染目标标志与此相反,但这仅适用于基于纹理的渲染目标)。在这里,我们指定颜色为黑色,深度为 1.0f,模板为 0(未使用)。最后一个参数 resourceUpdates 确保了在批次上记录的数据更新命令被提交。作为替代,我们也可以使用 QRhiCommandBuffer::resourceUpdate()。渲染目标针对一个换链,因此调用 currentFrameRenderTarget() 以获取一个有效的 QRhiRenderTarget

    cb->beginPass(m_sc->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, resourceUpdates);

记录三角形绘制调用很简单:设置管道、设置着色器资源、设置顶点/索引缓冲区(s),并记录绘制调用。在这里,我们使用一个非索引的绘制,只使用 3 个顶点。

    cb->setGraphicsPipeline(m_colorPipeline.get());
    cb->setShaderResources();
    const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
    cb->setVertexInput(0, 1, &vbufBinding);
    cb->draw(3);

    cb->endPass();

调用 setShaderResources() 没有给出参数,这意味着使用 m_colorTriSrb,因为这与活动的 QRhiGraphicsPipeline(《code translate="no">m_colorPipeline)相关联。

我们不会深入讨论全屏背景图像的渲染细节。有关示例代码,请参阅示例源代码。然而,值得注意的是一个纹理或缓冲区资源“调整大小”的通用模式。不存在调整现有本地资源大小的说法,因此调整纹理或缓冲区大小必须后跟一个创建(create())调用,以释放和重新创建底层的本地资源。为了确保 QRhiTexture 始终具有所需的大小,应用程序实现了以下逻辑。请注意,m_texture 在整个窗口的生命周期内保持有效,这意味着指向它的对象引用,例如在 QRhiShaderResourceBindings 中,始终是有效的。只有底层的本地资源在时间上兴起和消逝。

还要注意,我们在图像上设置了一个与我们要绘制的窗口相匹配的设备像素比。这确保了绘图代码可以不关心DPR,无论DPR如何,都会产生相同的布局,同时利用额外的像素来提高保真度。

void HelloWindow::ensureFullscreenTexture(const QSize &pixelSize, QRhiResourceUpdateBatch *u)
{
    if (m_texture && m_texture->pixelSize() == pixelSize)
        return;

    if (!m_texture)
        m_texture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, pixelSize));
    else
        m_texture->setPixelSize(pixelSize);

    m_texture->create();

    QImage image(pixelSize, QImage::Format_RGBA8888_Premultiplied);
    image.setDevicePixelRatio(devicePixelRatio());

一旦生成一个 QImage 并且在它内部完成基于 QPainter 的绘制后,我们使用 uploadTexture() 在资源更新批次上记录一个纹理上传操作。

    u->uploadTexture(m_texture.get(), image);

示例项目 @ code.qt.io

另请参阅 QRhiQRhiSwapChainQWindowQRhiCommandBufferQRhiResourceUpdateBatchQRhiBufferQRhiTexture

© 2024 The Qt Company Ltd。本文档中包含的贡献成果由各自所有者拥有版权。本提供的文档是根据Free Software Foundation发布的文档许可协议条款授权使用的GNU Free Documentation License版本1.3。Qt及其相关标志是The Qt Company Ltd在芬兰和其他国家/地区的商标。商标的所有其他商标均归其各自所有者所有。