Qt Quick 3D 架构

Qt Quick 3D 扩展了 Qt Quick 以支持 3D 内容的渲染。它添加了大量的功能,包括几个新的公共 QML 引入以及一个新的内部场景图和渲染器。本文档描述了从公共 API 到渲染管道工作原理的细节,介绍了 Qt Quick 3D 架构。

模块概述

Qt Quick 3D 由多个模块和插件组成,这些插件公开了额外的 3D API 以及导入现有 3D 资产的实用程序。

QML 引入

  • QtQuick3D - 包含 Qt Quick 3D 所有核心组件的主要引入
  • QtQuick3D.AssetUtils - 一个库,用于在运行时导入 3D 资产
  • QtQuick3D.Helpers - 一个库,提供额外的组件,可用于帮助设计和调试 3D 场景。

C++ 库

  • QtQuick3D - 唯一的公共 C++ 模块。包含所有暴露给 QtQuick3D QML 引入的类型以及一些 C++ API
  • QtQuick3DAssetImport - 一个内部和私有的库,用于帮助导入资产并将资产转换为 QML。
  • QtQuick3DRuntimeRender - 一个内部和私有的库,包含空间场景图节点和渲染器。
  • QtQuick3DUtils - 一个内部和私有的库,作为所有其他 C++ 模块的通用实用程序库。

资产导入插件

资产导入工具是通过插件架构实现的。Qt Quick 3D 中的插件扩展了资产导入库和工具(Balsam)的功能。

  • Assimp - 此插件使用第三方库 libAssimp 将 3D 格式转换为 Qt Quick 3D QML 组件。

Qt Quick 3D 如何融入 Qt 图形堆栈

上述示意图展示了 Qt Quick 3D 如何集成到更庞大的 Qt 图形堆栈中。Qt Quick 3D 作为 2D Qt Quick API 的扩展使用,当与 View3D 一起使用 3D 场景项目时,场景将通过 Qt 渲染硬件接口(RHI)进行渲染。RHI 可以将 API 调用转换为特定平台的正确本地渲染硬件 API 调用。上图显示了每个平台可用的选项。如果没有明确定义本地后端,Qt Quick 将默认为每个平台提供合适的本地后端进行渲染。

堆栈中 Qt Quick 3D 组件与 Qt Quick 堆栈之间的集成将在下一节中描述。

2D 中的 3D 集成

在 2D 中显示 3D 内容是 Qt Quick 3D API 的主要目的。将 3D 内容集成到 2D 的主要接口是 View3D 组件。

View3D 组件与其他任何具有内容的 QQuickItem 派生类一样工作,并实现了虚拟函数 QQuickItem::updatePaintNode。Qt Quick 在同步阶段期间调用 updatePaintNode 以处理 Qt Quick 场景图中的所有“脏”项目。这包括由 View3D 管理的 3D 项目,这些项目也会在 updatePaintNode 调用的结果下经历同步阶段。

View3D 的 updatePaintNode 方法执行以下操作

  • 如果尚不存在,则设置渲染器和渲染目标
  • 通过 SceneManager 同步 3D 场景中的项目
  • 更新由 Qt Quick 渲染的所有“动态”纹理(见下文的2D 在 3D 纹理路径

然而,3D 场景的渲染并不在 View3D 的 updatePaintNode 方法中发生。相反,updatePaintNode 返回一个包含 Qt Quick 3D 渲染器的 QSGNode 子类,该渲染器将在 Qt Quick 渲染过程的预处理阶段渲染 3D 场景。

Qt Quick 3D 如何进行渲染的管道取决于使用哪个 View3D::renderMode

离屏

View3D 的默认模式是 离屏。当使用离屏模式时,View3D 通过创建离屏表面并向其渲染来成为一个纹理提供者。该表面可以被映射为 Qt Quick 中的一个纹理,并通过 QSGSimpleTextureNode 进行渲染。

这种模式与 Qt Quick 中已有的 QSGLayerNodes 非常相似。

底层

当使用 底层 模式时,3D 场景将直接渲染到包含 View3DQQuickWindow。渲染是由于信号 QQuickWindow::beforeRenderPassRecording() 而发生的,这意味着 Qt Quick 中的其他内容将渲染在 3D 内容之上。

覆盖

当使用 覆盖 模式时,3D 场景将直接渲染到包含 View3DQQuickWindow。渲染是由于信号 QQuickWindow::afterRenderPassRecording() 而发生的,这意味着 3D 内容将渲染在所有其他 Qt Quick 内容之上。

内联

内联渲染模式使用QSGRenderNode,它允许直接渲染到Qt Quick的渲染目标,而不使用离屏表面。它通过在Qt Quick场景的2D渲染期间行内注入渲染命令来实现这一点。

这种模式可能导致问题,因为它使用与Qt Quick渲染器相同的深度缓冲区,而z值在Qt Quick与Qt Quick 3D中有完全不同的含义。

3D集成中的2D

在渲染3D场景时,有多个场景需要将2D元素嵌入到3D中。将2D内容集成到3D场景中有两种不同的方式,每种方式都有其到达屏幕的路径。

直接路径

直接路径用于将2D Qt Quick内容渲染成似乎存在于3D场景中作为一个平面项。例如,考虑以下场景定义

Node {
    Text {
        text: "Hello world!"
    }
}

这里发生的事情是:当在类型为QQuickItem的空间节点上设置子组件时,它首先被一个QQuick3DItem2D包装,它只是一个添加3D坐标到2D项的容器。这设置了所有后续2D子项渲染的基础3D变换,以便它们可以在3D场景中正确显示。

当渲染场景时,这些2D项的QSGNodes被传递给Qt Quick渲染器,以生成适当的渲染命令。因为这些命令是直接操作,并考虑当前3D变换,它们渲染得和在2D渲染器中完全一样,但是显示得像是在3D中一样。

这种方法的缺点是,无法使用3D场景的光照信息为2D内容添加阴影,因为Qt Quick 2D渲染器没有光照概念。

纹理路径

纹理路径使用一个2D Qt Quick场景来创建动态纹理内容。考虑以下纹理定义

Texture {
    sourceItem: Item {
        width: 256
        height: 256
        Text {
            anchors.centerIn: parent
            text: "Hello World!"
        }
    }
}

这种方法与Qt Quick中Layer项的工作方式相同,即将一切渲染到一个离屏表面,其大小与顶级Item相同,然后该离屏表面可作为一个可以在其他地方重复使用的纹理。

这个纹理可以由场景中的材料使用,以在项上渲染Qt Quick内容。

场景同步

场景管理器

Qt Quick 3D中的场景管理器负责跟踪3D场景中的空间项,并确保在同步阶段项目正在更新其相应的场景图节点。在Qt Quick中,对于2D情况,此角色由QQuickWindow执行。场景管理器是前端节点和后端场景图对象之间的主要接口。

每个View3D项目至少包含一个场景管理器,因为它会在构建时创建并与内置场景根相关联。当空间节点被添加为View3D的子节点时,它们将被注册到View3D的场景管理器中。当使用导入的场景时,将创建第二个SceneManager(如果已存在则引用)来管理不是View3D直接子节点的节点。这是因为与View3D不同,QQuickWindow上不存储导入的场景直到其被引用。额外的SceneManager确保导入场景中的资产在引用的每个QQuickWindow中至少创建一次。

虽然场景管理器是内部API,但了解场景管理器负责调用由调用update()方法标记为脏的所有对象的updateSpatialNode()是很重要的。

前端/后端同步

同步的目的是确保前端(Qt Quick)上设置的状态与后端(Qt Quick Spatial Scene Graph Renderer)上设置的状态相匹配。默认情况下,前端和后端位于不同的线程:前端在Qt主线程中,后端在Qt Quick的渲染线程中。同步阶段是主线程和渲染线程可以安全交换数据的地方。在这一阶段,场景管理器将为场景中每个脏节点调用updateSpatialNode()。这将为渲染器创建或更新一个新后端节点。

Qt Quick Spatial Scene Graph

Qt Quick 3D被设计为使用与Qt Quick相同的面向前端/后端的分离模式:前端对象由Qt Quick引擎控制,而后端对象包含渲染场景的状态数据。前端对象从QObject继承,并公开给Qt Quick引擎。源代码文件中的项映射到前端对象。

随着这些前端对象的属性更新,一个或多个后端节点被创建并放置到场景图中。因为渲染3D场景比渲染2D场景需要更多的状态,所以有一组专门的场景图节点来表示3D场景对象的状态。这个场景图被称为Qt Quick Spatial Scene Graph。

前端对象和后端节点都可以分为两类。第一类是空间类的,它们存在于空间的某个位置。在实践中,这意味着这些类型的每个都包含一个变换矩阵。对于空间项,父子关系是非常重要的,因为每个子项都继承了其父项的变换。

另一类项目是资源。资源项没有3D空间中的位置,而是使用其他项的状态。这些项之间可以有父子关系,但它的意义仅限于所有权。

与Qt Quick中的2D场景图不同,空间场景图直接向用户展示资源节点。例如,在Qt Quick中,虽然QSGTexture是公共API,但没有QQuickItem直接暴露该对象。相反,用户必须使用图像项,该图像项描述了纹理的来源以及如何渲染它,或者编写对QSGTexture本身进行操作的C++代码。在Qt Quick 3D中,这些资源直接在QML API中暴露。这是必要的,因为资源是场景状态的重要部分。这些资源可以由场景中的许多对象引用:例如,许多材料可以重用同一纹理。也可以在运行时更改纹理的属性,从而直接改变纹理的采样方式,例如。

空间对象

所有空间对象都是Node组件的子类,它包含定义3D空间中位置、旋转和缩放属性的特性。

资源对象

资源对象是Object3D组件的子类。Object3D只是QObject的一个子类,具有一些用于与场景管理器一起使用的特殊帮助器。资源对象确实有父/子关联,但这些关联主要用于资源所有权。

3D视图和渲染层

关于前端/后端的分离,从用户角度来看,View3D是分离点,因为View3D定义了要渲染的场景内容。在Qt Quick空间场景图中,将要渲染的场景的根节点是层节点。层节点是由View3D使用View3D的属性和SceneEnvironment的属性组合创建的。当为View3D渲染场景时,传给渲染器的实际上是这个层节点来完成场景的渲染。

场景渲染

设置渲染目标

渲染过程的第一步是确定和设置场景渲染目标。根据在SceneEnvironment中设置的哪个属性,实际的渲染目标会有所不同。第一个决定是内容是否直接渲染到窗口表面,或渲染到离屏纹理。默认情况下,View3D将渲染到离屏纹理。在使用后处理效果时,渲染到离屏纹理是强制性的。

一旦确定了场景渲染目标,则会设置一些全局状态。

  • 窗口大小 - 如果渲染到窗口
  • 视口 - 正在被渲染的场景区域的尺寸
  • 裁剪矩形 - 视口应该裁剪到的窗口的子集
  • 清屏颜色 - 如果有的话,使用什么颜色清除渲染目标。

准备渲染

渲染的下一阶段是准备阶段,在这个阶段,渲染器会进行维护工作来确定给定帧需要渲染什么,同时确保所有必要资源都可用且是最新的。

准备阶段本身有两个阶段:确定要渲染的内容和所需资源的概要准备;以及使用RHI实际设置渲染管线和缓冲区,以及设置主要场景通过的渲染依赖的细节准备。

高级渲染准备

这一阶段的目的是从空间场景图中提取状态,以便用于创建渲染命令。这里的概要是渲染器从单个摄像头和一组光照状态的视角,创建要渲染的几何形状和材料组合的列表。

首先,需要确定所有内容的全局通用状态。如果 场景环境 定义了一个 光探针,则它会检查与该光探针纹理关联的环境图是否已加载,如果没有,则加载或生成一个新的环境图。环境图的生成本身将是一组将源纹理卷积到立方图的步骤。这个立方图将包含镜面反射信息和辐射,用于材料着色。

接下来,渲染器需要确定场景中要使用哪个摄像头。如果没有通过 View3D 明确定义活动摄像头,则使用场景中可用的第一个摄像头。如果没有摄像头在场景中,则不渲染任何内容,渲染器会退出。

确定摄像头后,就可以计算这帧的投影矩阵。计算在这里进行是因为每个可渲染元素都需要知道如何投影。这也意味着现在可以计算哪些可渲染元素应该被渲染。从所有可渲染元素的列表开始,我们移除所有不可见的元素,因为它们可能已禁用或完全透明。然后,如果活动摄像头启用了视锥剔除,会检查每个可渲染元素是否完全位于摄像头的视锥外部,如果是,则将其从渲染列表中移除。

除了摄像头投影,摄像头的方向也被计算,因为这在着色代码中的光照计算是必要的。

如果场景中存在光节点,这些节点将被收集到一个长度为最大可用光量数的列表中。如果场景中的光节点多于渲染器支持的光量,则超出限制的光节点将被忽略,不影响场景的光照。可以指定光节点的范围,但请注意,即使在设置范围时,每个光的光照状态仍发送到具有光照的每个材料,但对于不在范围内的光,其亮度将被设置为0,因此实际上这些光不会对那些材料的照明产生影响。

现在有一个可能较短的渲染列表,需要更新每个列表项以反映场景的当前状态。对于每个可渲染元素,我们检查是否已加载一个适合的材料,如果没有,则创建一个新的。材料是着色器和渲染管线的组合,它是创建绘制调用所需的,此外,渲染器确保需要渲染可渲染元素的所有资源已加载,例如模型上的几何形状和纹理。尚未加载的资源在这里加载。

然后,可渲染列表被排序成3个列表。

  • 不透明项:这些项目从前到后排序,换句话说,从靠近相机到远离相机的项目。这样做是为了利用硬件遮挡剔除或片段着色器中的早期z检测。
  • 2D项:这些是由Qt Quick渲染器绘制的QtQuick项。
  • 透明项:这些项目从后往前排序,换句话说,从最远离相机的项目到最近的项目。这样做是因为透明项需要与其后面的所有项混合。

低级渲染准备

现在已经确定了需要考虑此帧的所有内容,可以解决主渲染通道的管道和依赖项。此阶段的第一件事是渲染主通道所需的任何预通道。

  • 渲染深度通道 - 一些功能,如屏幕空间环境遮挡和阴影,需要一个深度预通道。此通道包括将所有不透明项渲染到深度纹理中。
  • 渲染SSAO通道 - 屏幕空间环境遮挡通道的目的是生成环境遮挡纹理。此纹理稍后由材料用于在着色时变暗某些区域。
  • 渲染阴影通道 - 场景中每个启用了阴影的光源都贡献了一个额外的阴影通道。渲染器采用了两种不同的阴影技术,因此,根据光类型的不同,将会有不同的通道。当从方向光渲染阴影时,场景将会从一个结合方向光的方向和相机视锥体大小的2D遮挡纹理中渲染。当从点光源或聚光灯渲染阴影时,光的遮挡纹理是表示相对于光每一个面方向遮挡贡献的立方体贴图。
  • 渲染屏幕纹理 - 此通道仅在使用需要屏幕纹理的自定义材质时发生,该纹理可用于渲染技术,如折射。此通道的工作方式类似于深度通道,但将所有不透明项渲染到颜色纹理中。

在本地渲染完成后,其余通道已准备就绪但尚未渲染。此准备包括将高级准备阶段收集到的状态转换为图形原语,如创建/更新统一缓冲区值、将采样器与依赖纹理关联、设置着色器资源绑定以及创建用于执行绘制调用的管道状态所需的所有其他内容。

场景渲染

准备工作已经完成,现在可以轻松运行影响主场景内容的命令。渲染工作是按照以下顺序工作的

  • 清除通道 - 这实际上不是一个通道,但根据SceneEnvironment上的backgroundMode的设置,这里可能发生不同的事情。如果背景模式设置为透明或颜色,则将使用透明度或指定的颜色清除颜色缓冲区。如果背景模式设置为SkyBox,则将运行一个通道,从相机的视角渲染SkyBox,同时用初始数据填充缓冲区。
  • 不透明通道 - 接下来将绘制所有不透明项。这仅涉及到设置管道状态,然后按列表顺序运行每个项目的绘制命令,因为它们已经在这一点上进行了排序。
  • 2D通道 - 如果场景中存在2D项,则将调用Qt Quick渲染器以生成必要的渲染命令,以渲染这些项。
  • 透明通道 - 最后,将按相同的方式逐个渲染场景中的透明项。

场景渲染结束。

后处理

如果启用了任何后处理功能,则可以假设场景渲染器的结果是用于后处理阶段的纹理。所有后处理方法都是在此场景输入纹理上操作的附加遍历。

后处理阶段的每个步骤都是可选的,如果没有启用内置功能并且没有用户定义的效果,那么场景渲染的输出将被用作最终渲染目标的输出。然而请注意,色调映射默认启用。

内置后处理

ExtendedSceneEnvironment及其父类SceneEnvironment提供了3D场景中最常用的效果,以及将渲染器生成的高动态范围颜色值映射到0-1 LDR范围的颜色映射。这些效果包括景深、发光/光照、镜头晕光、电影边框、色彩调整和分级、雾以及环境遮蔽。

后处理效果

应用程序可以在SceneEnvironment::effects属性中指定自己的自定义后处理效果作为有序列表。当此列表不为空时,其中的效果将先于ExtendedSceneEnvironment提供的内置效果应用。每个后处理效果都是链的一部分,前一个效果输出的结果将成为下一个效果的输入。链中的第一个效果直接从场景渲染步骤的输出获取输入。效果也可以访问场景渲染器的深度纹理输出。

此过程中的每个效果可以由多个子遍历组成,这意味着可以将内容渲染到中间缓冲区中。多遍历效果的最后一个遍历应该输出一个包含要用于后处理阶段下一步骤的颜色数据的单个纹理。

时间累积和渐进式抗锯齿

通过在SceneEnvironment中设置属性,可以可选地启用时间累积和渐进式抗锯齿步骤。尽管它不是后处理阶段的一部分,但时间累积和渐进式抗锯齿的实现在后处理阶段。

在场景正在被积极更新时执行时间抗锯齿。当启用时,活动相机 gegen每帧都会对相机方向进行非常小的调整,同时绘制场景。然后将当前帧与之前渲染的帧混合以平滑渲染的内容。

仅当场景没有更新时才执行渐进式抗锯齿。当启用时,将强制进行更新,并且对活动相机的方向进行非常小的调整以渲染场景的当前状态。最多积累8帧,并与预定义的权重混合。这会使非动画场景变得平滑,但会以性能成本为代价,因为每次更新都会渲染几个额外的帧。

超采样抗锯齿(SSAA)

超采样抗锯齿是一种强制平滑场景的方法。它的工作原理是对一个纹理进行渲染,该纹理的大小是场景请求大小的倍数,然后对其进行下采样以达到目标大小。例如,如果请求2X SSAA,那么场景将被渲染到一个大小为预期大小两倍的纹理中,然后在这一阶段中进行下采样。这可能会对性能和资源使用产生巨大影响,因此应尽可能避免使用。另外,由于用于此方法的纹理可能大于渲染硬件支持的大小,因此可能无法使用这种方法来设置View3D的大小。

© 2024 Qt公司有限公司。此处包含的文档贡献均为各自所有者的版权。此处提供的文档是根据自由软件基金会发布的GNU自由文档许可证第1.3版许可的。Qt及其相应标志是芬兰及世界其他国家的Qt公司有限公司的商标。所有其他商标均为其各自所有者的财产。