Qt Quick 3D 架构
Qt Quick 3D 扩展了 ,以支持 3D 内容的渲染。它增加了大量功能,包括几个新的公共 QML 导入以及一个新的内部场景图和渲染器。本文档介绍了 的架构,从公共 API 到渲染管道的工作细节。Qt Quick Qt Quick 3D
模块概述
Qt Quick 3D 由多个模块和插件组成,这些模块和插件公开了额外的 3D API 以及用于调节和导入现有 3D 资产的实用程序。
QML 导入
- QtQuick3D - 主导入包含以下所有核心组件Qt Quick 3D
- QtQuick3D.AssetUtils - 运行时导入 3D 资产的库
- QtQuick3D.Helpers - 附加组件库,可用于帮助设计 3D 和调试 3D 场景。
C++ 库
- QtQuick3D - 唯一的公共 C++ 模块。包含 QML 导入的所有类型定义以及一些 C++ 应用程序接口QtQuick3D
- QQuick3DGeometry - 创建过程网格数据的子类
- QQuick3DTextureData - 创建程序纹理数据的子类
- QQuick3D::idealSurfaceFormat - 用于获取理想曲面格式的子类
QtQuick3DAssetImport
- 一个内部私有库,用于帮助导入资产并将资产转换为 QML。QtQuick3DRuntimeRender
- 一个内部私有库,包含空间场景图节点和渲染器。QtQuick3DUtils
- 一个内部私有库,用作所有其他 C++ 模块的通用工具库。
资产导入插件
资产导入工具是通过基于插件的架构实现的。Qt Quick 3D 提供的插件扩展了资产导入程序库和工具Balsam 的功能。
- Assimp - 该插件使用第三方库 libAssimp 将 3D 交换格式的 3D 资产转换为Qt Quick 3D QML 组件。
Qt Quick 3D 如何融入 Qt XML 图形堆栈
上图说明了Qt Quick 3D 如何融入更大的 Qt 图形堆栈。Qt Quick 3D 作为 2DQt Quick API 的扩展,当结合View3D 使用 3D 场景项时,场景将通过 Qt Rendering Hardware Interface (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 会为Qt Quick 场景图中的所有 "脏 "项调用 updatePaintNode。这包括由View3D 管理的 3D 项目,这些项目也会因 updatePaintNode 调用而进入同步阶段。
View3D 的 updatePaintNode 方法执行以下操作:
- 如果还没有呈现器和呈现目标,则设置呈现器和呈现目标
- 通过 SceneManager 同步 3D 场景中的项目
- 更新由Qt Quick 渲染的任何 "动态 "纹理(下面三维纹理路径中的二维纹理)。
不过,三维场景的渲染并不发生在View3D updatePaintNode 方法中。updatePaintNode 返回一个包含Qt Quick 3D 渲染器的QSGNode 子类,该子类将在Qt Quick 渲染过程的预处理阶段渲染三维场景。
Qt Quick 3D 如何渲染取决于所使用的View3D::renderMode :
屏幕外
View3D 的默认模式是Offscreen 。使用离屏模式时,View3D 会创建一个离屏表面并渲染至该表面,从而成为纹理提供者。该表面可在Qt Quick 中映射为纹理,并通过QSGSimpleTextureNode 渲染。
这种模式与Qt Quick 中 QSGLayerNodes 的工作方式非常相似。
底层
当使用Underlay 模式时,三维场景会直接渲染到包含View3D 的QQuickWindow 中。渲染是作为信号QQuickWindow::beforeRenderPassRecording() 的结果进行的,这意味着Qt Quick 中的所有其他内容都将渲染到三维内容之上。
叠加
使用Overlay 模式时,三维场景会直接渲染到包含View3D 的QQuickWindow 中。渲染会根据信号QQuickWindow::afterRenderPassRecording() 进行,这意味着三维内容会渲染到所有其他Qt Quick 内容之上。
内联
Inline 渲染模式使用QSGRenderNode ,可直接渲染到Qt Quick 的渲染目标,而无需使用屏幕外表面。它通过在Qt Quick 场景的 2D 渲染过程中内联注入渲染命令来实现这一功能。
这种模式可能存在问题,因为它使用与Qt Quick 渲染器相同的深度缓冲区,而 Z 值在Qt Quick 和Qt Quick 3D 中的含义完全不同。
3D 中的 2D 整合
在渲染三维场景时,有许多场景需要将二维元素嵌入三维。有两种不同的方法可以将 2D 内容集成到 3D 场景中,每种方法都有自己的通向屏幕的路径。
直接路径
直接路径用于渲染 2DQt Quick 内容,就像它作为一个平面项目存在于 3D 场景中一样。例如,请看下面的场景定义:
Node {
Text {
text: "Hello world!"
}
}
这里发生的情况是:当一个子组件被设置在QQuickItem 类型的空间节点上时,它首先会被一个 QQuick3DItem2D 封装,而 QQuick3DItem2D 只是一个将 3D 坐标添加到 2D 项目的容器。这将为所有后续 2D 子项的渲染设置基础 3D 变换,以便它们能正确地显示在 3D 场景中。
当需要渲染场景时,这些 2D 项的 QSGNodes 会被传递到Qt Quick 渲染器,以生成相应的渲染命令。由于这些命令是内联完成的,并且考虑到了当前的三维变换,因此它们的渲染效果与二维渲染器中的效果完全相同,但显示出来的效果却像是在三维场景中渲染的一样。
这种方法的缺点是,由于Qt Quick 2D 渲染器没有光照的概念,因此 3D 场景的光照信息无法用于为 2D 内容遮光。
纹理路径
纹理路径使用 2DQt Quick 场景来创建动态纹理内容。请看下面的纹理定义:
Texture { sourceItem: Item { width: 256 height: 256 Text { anchors.centerIn: parent text: "Hello World!" } } }
这种方法与Qt Quick 中图层项的工作方式相同,即所有内容都被渲染到一个与顶层项大小相同的屏幕外表面,然后该屏幕外表面可作为纹理在其他地方重复使用。
场景中的材质可使用此纹理在项目上渲染Qt Quick 内容。
场景同步
场景管理器
Qt Quick 3D 中的场景管理器负责跟踪三维场景中的空间项目,并确保项目在同步阶段更新其相应的场景图节点。在Qt Quick 中,这一角色由QQuickWindow 在二维情况下担任。场景管理器是前端节点和后端场景图对象之间的主要接口。
每个View3D 项目至少有一个场景管理器,因为在构建时会创建一个场景管理器并与内置场景根相关联。当空间节点被添加为View3D 的子节点时,它们将在View3D 的场景管理器中注册。在使用导入的场景时,需要创建第二个 SceneManager(如果已经存在,则引用)来管理不 是View3D 直接子节点的节点。之所以需要这样做,是因为与View3D 不同,导入的场景在被引用之前并不存在于QQuickWindow 上。附加的 SceneManager 可确保属于导入场景的资产在每个QQuickWindow 中被引用时至少创建一次。
虽然场景管理器是一个内部 API,但重要的是要知道场景管理器负责调用 update() 方法对所有已标记为 dirty 的对象进行 updateSpatialNode 操作。
前台/后台同步
同步的目的是确保前端(Qt Quick )设置的状态与后端(Qt Quick Spatial Scene Graph Renderer)设置的状态一致。默认情况下,前端和后端生活在不同的线程中:前端在 Qt XML 主线程中,后端在Qt Quick 的渲染线程中。同步阶段是主线程和呈现线程安全交换数据的阶段。在此阶段,场景管理器将为场景中的每个脏节点调用 updateSpatialNode。这将创建一个新的后端节点,或更新现有节点供呈现器使用。
Qt Quick 空间场景图
Qt Quick 3D 在设计上使用与 相同的前端/后端分离模式:前端对象由 引擎控制,而后端对象包含用于渲染场景的状态数据。前台对象继承自 ,并暴露给 引擎。QML 源文件中的项目直接映射到前台对象。Qt Quick Qt Quick QObject Qt Quick
当这些前台对象的属性更新时,就会创建一个或多个后台节点,并将其放入场景图中。由于渲染 3D 场景比渲染 2D 场景涉及更多的状态,因此有一套单独的专门场景图节点来表示 3D 场景对象的状态。这种场景图被称为Qt Quick 空间场景图。
前台对象和后台节点可分为两类。第一类是空间类,即它们存在于三维空间中的某处。实际上,这意味着每种类型都包含一个变换矩阵。对于空间项来说,父子关系非常重要,因为每个子项都会继承父项的变换。
另一类项目是资源。资源项在三维空间中没有位置,而只是其他项使用的状态。这些项目之间可能存在父子关系,但除了所有权之外没有其他意义。
与Qt Quick 中的二维场景图不同,空间场景图直接向用户公开资源节点。因此,例如在Qt Quick 中,虽然QSGTexture 是公共 API,但并没有QQuickItem 可以直接公开这个对象。取而代之的是,用户必须使用图像项(该图像项描述了纹理的来源以及如何渲染纹理),或者编写 C++ 代码对QSGTexture 本身进行操作。在Qt Quick 3D 中,这些资源直接暴露在 QML API 中。这是必要的,因为资源是场景状态的重要组成部分。场景中的许多对象都可以引用这些资源:例如,许多材质(Materials)可以使用相同的纹理(Texture)。也可以在运行时设置纹理的属性,例如直接改变纹理的采样方式。
空间对象
所有空间对象都是节点组件的子类,节点组件包含定义三维空间中位置、旋转和缩放的属性。
资源对象
资源对象是Object3D 组件的子类。Object3D 只是QObject 的子类,其中包含一些与场景管理器配合使用的特殊助手。资源对象确实有父/子关联,但这些关联主要用于资源所有权。
View3D 和渲染图层
关于前端/后端分离,从用户角度来看,View3D 是分离点,因为View3D 定义了要渲染的场景内容。在Qt Quick 空间场景图中,要渲染的场景的根节点是图层节点。图层节点由View3D 使用View3D 的属性和SceneEnvironment 的属性组合创建。在为View3D 渲染场景时,正是这个图层节点被传递给渲染器以渲染场景。
场景渲染
设置渲染目标
渲染过程的第一步是确定和设置场景渲染目标。根据SceneEnvironment 中设置的属性,实际的渲染目标会有所不同。首先要决定是将内容直接呈现到窗口表面,还是呈现到屏幕外的纹理。默认情况下,View3D 将渲染到屏幕外纹理。在使用后期处理特效时,必须将内容呈现到屏幕外纹理上。
一旦确定了场景渲染目标,就会设置一些全局状态。
- 窗口大小 - 如果渲染到窗口
- 视口 - 渲染场景区域的大小
- 剪切矩形 - 视口应剪切到的窗口子集
- clear color(清除颜色)- 清除渲染目标的颜色(如果有)。
准备渲染
渲染的下一个阶段是准备阶段,渲染器会在此阶段进行内务整理,以确定特定帧需要渲染的内容,以及所有必要的资源是否可用和最新。
准备阶段本身分为两个阶段:确定要渲染的内容和所需资源的高级准备阶段;使用 RHI 实际设置渲染管道和缓冲区以及设置主场景传递的渲染依赖关系的低级准备阶段。
高级渲染准备
这一阶段的目的是将空间场景图的状态提取为可用于创建渲染命令的内容。这里的概述是,渲染器正在创建几何体和材质组合列表,以便从具有一组照明状态的单个摄像机的角度进行渲染。
首先要做的是确定所有内容的全局通用状态。如果SceneEnvironment 定义了lightProbe ,则会检查与该探针纹理相关的环境贴图是否已加载,如果未加载,则会加载或生成新的环境贴图。生成环境贴图的过程本身就是将源纹理卷积为立方体贴图的一系列过程。立方体贴图将包含镜面反射信息和辐照度信息,后者用于材质着色。
接下来,渲染器需要确定使用场景中的哪个摄像机。如果View3D 没有明确定义活动相机,则会使用场景中第一个可用的相机。如果场景中没有摄像机,则不会渲染任何内容,渲染器也会退出。
确定摄像机后,就可以计算该帧的投影矩阵。此时进行计算是因为每个可渲染对象都需要知道如何进行投影。这也意味着现在可以计算应该渲染哪些渲染项了。从所有可呈现项目的列表开始,我们会删除所有由于禁用或完全透明而不可见的项目。然后,如果在活动摄像机上启用了缩放功能,则会检查每个可呈现项目是否完全位于摄像机缩放视图之外,如果是,则将其从可呈现列表中删除。
除了摄像机投影外,摄像机方向也会被计算出来,因为这对于着色代码中的光照计算是必要的。
如果场景中存在灯光节点,这些节点会被收集到一个列表中,长度与可用灯光的最大值相同。如果场景中的灯光节点多于渲染器支持的灯光数量,那么超过该限制的任何额外灯光节点都会被忽略,不会对场景的照明产生影响。我们可以指定灯光节点的范围,但要注意的是,即使设置了范围,每个灯光的照明状态仍会发送到每个有照明的材质,但对于不在范围内的灯光,其亮度将被设置为 0,因此实际上这些灯光不会对这些材质的照明产生影响。
现在有了一个希望更短的可呈现效果的列表,每个项目都需要更新,以反映场景的当前状态。对于每个可呈现效果,我们都会检查是否加载了合适的材质,如果没有,就会创建一个新的材质。材质是着色器和渲染管道的组合,创建绘制调用时需要用到它。此外,渲染器还会确保已加载渲染可渲染对象所需的资源,例如模型上设置的几何体和纹理。尚未加载的资源将在此处加载。
然后,呈现器列表会被排序为 3 个列表。
- 不透明项目:这些项目从前向后排序,换句话说,就是从离摄像机最近的项目到离摄像机最远的项目。这样做是为了利用片段着色器中的硬件遮挡剔除或早期 Z 检测功能。
- 2D 项目:这些是由Qt Quick 渲染器渲染的QtQuick 项目。
- 透明项目:这些项目从后向前排序,换句话说,就是从离摄像机最远的项目到离摄像机最近的项目。这样做是因为透明项目需要与它们后面的所有项目混合。
低层渲染准备
在确定了这一帧需要考虑的所有事项后,就可以处理主要渲染通道的管道和依赖关系了。在这一阶段,首先要做的是渲染主渲染所需的预处理。
- 渲染深度预处理 - 某些功能(如屏幕空间环境遮蔽和阴影)需要深度预处理。该过程包括将所有不透明项目渲染为深度纹理。
- 渲染 SSAOPass - 屏幕空间环境遮蔽通道的目的是生成环境遮蔽纹理。随后,材质在着色时会使用此纹理使某些区域变暗。
- 渲染阴影通道(Render ShadowPasses)- 场景中启用阴影的每个灯光都会产生额外的阴影通道。渲染器采用了两种不同的阴影技术,因此根据光线类型的不同,会有不同的通道。当渲染来自定向光的阴影时,场景会根据定向光的方向和摄像机光斑的大小组合渲染成 2D 遮挡纹理。当渲染来自点光源或聚光灯的阴影时,灯光的遮蔽纹理是一个立方体贴图,代表了相对于灯光每个面方向的遮蔽贡献。
- 渲染屏幕纹理(Render ScreenTexture)--只有在使用需要屏幕纹理的CustomMaterial 时,才会出现此过程,屏幕纹理可用于折射等渲染技术。此通道的工作原理与深度通道类似,但会将所有不透明项目渲染为彩色纹理。
在完成依赖关系渲染后,其余通道将进行准备工作,但不会渲染。准备工作包括将高级准备阶段收集的状态转化为图形原语,如创建/更新统一缓冲区值、将采样器与依赖纹理关联、设置着色器资源绑定,以及创建执行绘制调用所需的管道状态所涉及的其他一切。
场景渲染
既然前期准备工作已经完成,那么最简单的部分就是运行为主场景内容的命令。渲染按以下顺序进行
- Clear Pass(清除通道)--这并不是一个真正的通道,但根据SceneEnvironment 上设置的背景模式(backgroundMode),这里会发生不同的情况。如果背景模式是透明或彩色,那么颜色缓冲区将以指定的透明度或颜色清除。但是,如果背景模式设置为 SkyBox,那么将运行一个通道,从摄像机的角度渲染 SkyBox,这也将用初始数据填充缓冲区。
- 不透明通道 - 接下来将绘制所有不透明项目。这只涉及设置管道状态,并按照列表中的顺序为每个项目运行绘制命令,因为此时它们已经排序。
- 2D 通道 - 如果场景中有任何 2D 项目,则会调用Qt Quick 渲染器生成渲染这些项目所需的渲染命令。
- 透明通道 - 最后,场景中的透明项目将以与不透明项目相同的方式逐个渲染。
至此,场景渲染结束。
后期处理
如果启用了任何后处理功能,那么可以认为场景渲染器的结果就是纹理,而纹理是后处理阶段的输入。所有的后处理方法都是对该场景输入纹理进行操作的附加通道。
后处理阶段的所有步骤都是可选的,如果没有启用内置功能和用户自定义特效,场景渲染的输出结果就是最终渲染目标所使用的结果。但请注意,tonemapping 默认是启用的。
内置后期处理
ExtendedSceneEnvironment 及其父类型 提供 3D 场景中最常用的特效,以及用于将渲染器生成的高动态范围色彩值映射到 0-1 LDR 范围的色调映射。这些效果包括景深、发光/炽热、镜头耀斑、渐晕、色彩调整和分级、雾和环境遮蔽。SceneEnvironment
后期处理效果
应用程序可在SceneEnvironment::effects 属性中以有序列表的形式指定自己的自定义后期处理效果。当该列表非空时,其中的特效将在ExtendedSceneEnvironment 提供的内置特效之前应用。每个后处理效果都是一个链条的一部分,上一个效果的输出是下一个效果的输入。该链中的第一个特效直接从场景渲染器步骤的输出中获取输入。特效也可以访问场景渲染器的深度纹理输出。
此流程中的每个特效都可以由多个子通道组成,这意味着可以将内容渲染到中间缓冲区。多通道特效的最终通道将输出包含色彩数据的单一纹理,供后处理阶段的下一步使用。
时序抗锯齿和渐进抗锯齿
时序抗锯齿和渐进抗锯齿步骤可通过在SceneEnvironment 中设置属性来启用。虽然严格来说,时序抗锯齿和渐进抗锯齿并不是后处理阶段的一部分,但其实际效果是在后处理阶段实现的。
时态抗锯齿在场景主动更新时执行。启用时,活动摄像机在绘制场景时会对每一帧的摄像机方向进行很小的调整。然后将当前帧与之前渲染的帧混合,以平滑渲染的内容。
渐进式抗锯齿仅在场景未更新时执行。启用后,将强制进行更新,并通过对活动摄像机方向的微小调整来渲染场景的当前状态。最多可累积 8 个帧,并以预定义的权重混合在一起。这具有平滑非动画场景的效果,但需要付出性能代价,因为每次更新都要渲染多个额外帧。
超级采样抗锯齿(SSAA)
超级采样抗锯齿是一种强行平滑场景的方法。它的工作原理是渲染到场景所需大 小倍数的纹理,然后再降低采样到目标大小。例如,如果要求 2 倍的 SSAA,那么场景将被渲染为目标尺寸 2 倍的纹理,然后在此阶段进行降采样。这会对性能和资源使用产生巨大影响,因此应尽可能避免。View3D 尺寸过大也可能导致无法使用此方法,因为此方法所需的纹理可能大于渲染硬件所支持的尺寸。
© 2025 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.