本页内容

Qt Quick 3D 架构

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

模块概述

Qt Quick 3D 由多个模块和插件组成,这些组件提供了额外的 3D API,以及用于处理和导入现有 3D 资源的实用工具。

QML 导入

C++ 库

  • QtQuick3D - 唯一的公开 C++ 模块。包含所有向QtQuick3D QML 导入公开的类型定义,以及若干 C++ API
  • QtQuick3DAssetImport - 一个内部且私有的库,用于辅助导入资源并将资源转换为 QML 格式。
  • QtQuick3DRuntimeRender - 一个内部且私有的库,包含空间场景图节点和渲染器。
  • QtQuick3DUtils - 一个内部且私有的库,作为所有其他 C++ 模块共用的通用工具库。

资源导入器插件

资产导入工具采用基于插件的架构实现。Qt Quick 3D 随附的插件扩展了资产导入库及工具Balsam 的功能。

  • Assimp - 该插件使用第三方库 libAssimp,将 3D 交换格式中的 3D 资源转换为Qt Quick 3D QML 组件。

Qt Quick 3D 在 Qt 图形堆栈中的定位

<span translate=Qt Quick 3D 图形栈架构图" src="images/quick3d-graphics-stack.drawio.svg" title="Qt Quick 3D 图形栈架构图"/>

上图说明了Qt Quick 3D 在更广泛的Qt图形栈中的定位。Qt Quick 3D 作为2DQt Quick API的扩展而工作;当将3D场景项与View3D 结合使用时,场景将通过Qt渲染硬件接口(RHI)进行渲染。 RHI 会将 API 调用转换为针对特定平台的正确原生渲染硬件 API 调用。上图展示了各平台可用的选项。如果未显式定义原生后端,则Qt Quick 会为每个平台默认选择一个合理的原生渲染后端。

栈中Qt Quick 3D 组件与Qt Quick 栈之间的集成将在下文各节中进行描述。

3D 与 2D 的集成

在 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 渲染的任何“动态”纹理(如下文“3D 纹理路径中的 2D纹理”)

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

Qt Quick 3D 的渲染机制取决于所使用的View3D::renderMode 实例:

离屏模式

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

这种模式与Qt Quick 中 QSGLayerNodes 的现有工作原理非常接近。

底层

在使用Underlay 模式时,3D 场景会直接渲染到包含View3D 的QQuickWindow 中。渲染是由信号QQuickWindow::beforeRenderPassRecording() 触发的,这意味着Qt Quick 中的其他所有内容都将渲染在 3D 内容之上。

叠加

在使用Overlay 模式时,3D场景会直接渲染到包含View3D 的QQuickWindow 中。渲染是由信号QQuickWindow::afterRenderPassRecording()触发的,这意味着3D内容将渲染在Qt Quick 中所有其他内容之上。

内联

Inline 渲染模式使用QSGRenderNode ,该模式支持直接渲染到Qt Quick 的渲染目标,无需使用离屏表面。其实现方式是在Qt Quick 场景的2D渲染过程中,内联注入渲染命令。

此模式可能会引发问题,因为它与Qt Quick 渲染器使用相同的深度缓冲区,而Qt Quick 和Qt Quick 3D 中z值的含义截然不同。

2D 与 3D 的集成

在渲染 3D 场景时,许多情况下需要将 2D 元素嵌入到 3D 场景中。将 2D 内容集成到 3D 场景中有两种不同的方法,每种方法都有各自的渲染路径。

直接路径

直接路径用于渲染 2DQt Quick 内容,使其仿佛作为 3D 场景中的平面对象存在。例如,考虑以下场景定义:

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

其工作原理如下:当子组件被设置在类型为 `QQuickItem` 的空间节点上时,它首先会被 `QQuick3DItem2D` 包裹,该组件仅是一个为 2D 项添加 3D 坐标的容器。 这为后续所有 2D 子元素的渲染设置了基础 3D 变换,从而确保它们在 3D 场景中正确显示。

在渲染场景时,这些 2D 项的 QSGNodes 会被传递给Qt Quick 渲染器,以生成相应的渲染命令。 由于这些命令是在内联中执行的,并且考虑了当前的 3D 变换,因此它们的渲染效果与 2D 渲染器完全相同,但显示效果却仿佛是在 3D 环境中渲染的一样。

这种方法的缺点是,无法使用 3D 场景的照明信息来为 2D 内容着色,因为Qt Quick 2D 渲染器没有照明的概念。

纹理路径

纹理路径利用Qt Quick 的2D场景来创建动态纹理内容。请看以下纹理定义:

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

这种方法的工作原理与Qt Quick 中“图层”项(Layer items)类似:所有内容都会渲染到一个与顶级项(Item)大小相同的离屏表面上,该离屏表面随后可作为纹理在其他地方重复使用。

随后,场景中的材质即可使用该纹理,在对象上渲染Qt Quick 内容。

场景同步

场景管理器

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

每个View3D 对象至少拥有一个场景管理器,因为在对象构造时会创建一个场景管理器并将其与内置场景根节点关联。当空间节点作为View3D 的子节点添加时,它们会被注册到View3D 的场景管理器中。 使用导入的场景时,会创建第二个场景管理器(如果已存在则引用该管理器),用于管理那些不是View3D 直接子节点的节点。这是必要的,因为与View3D 不同,导入的场景在被引用之前并不存在于QQuickWindow 上。 额外的 SceneManager 可确保,对于每个引用了该导入场景的QQuickWindow ,属于该场景的资源至少会被创建一次。

虽然场景管理器是一个内部 API,但需要注意的是,场景管理器负责对所有通过调用 update() 方法被标记为“已修改”的对象调用 updateSpatialNode。

前端/后端同步

同步的目标是确保前端(Qt Quick )设置的状态与后端(Qt Quick 空间场景图渲染器)设置的状态一致。默认情况下,前端和后端运行在不同的线程中:前端在 Qt 主线程中,后端在Qt Quick 的渲染线程中。 同步阶段是主线程和渲染线程可以安全交换数据的阶段。在此阶段,场景管理器将针对场景中的每个已修改节点调用 updateSpatialNode。这将创建一个新的后端节点,或更新现有节点,供渲染器使用。

Qt Quick 空间场景图

Qt Quick 3D 的设计采用了与Qt Quick 相同的前端/后端分离模式:前端对象由Qt Quick 引擎控制,而后端对象则包含用于渲染场景的状态数据。前端对象继承自QObject ,并暴露给Qt Quick 引擎。QML源文件中的项会直接映射到前端对象。

随着这些前端对象属性的更新,会创建一个或多个后端节点并将其放置到场景图中。由于渲染 3D 场景涉及的状态远多于渲染 2D 场景,因此有一套专门的场景图节点用于表示 3D 场景对象的状态。 该场景图被称为“Qt Quick 空间场景图”。

前端对象和后端节点均可分为两类。第一类是空间对象,其存在于三维空间的某个位置。实际上,这意味着这两类对象都包含一个变换矩阵。对于空间对象而言,父子关系至关重要,因为每个子对象都会继承其父对象的变换。

另一类对象是资源。资源对象在3D空间中没有具体位置,而仅仅是供其他对象使用的状态。这些对象之间可以存在父子关系,但这种关系仅表示所有权,并无其他含义。

与Qt Quick 中的 2D 场景图不同,空间场景图会直接向用户暴露资源节点。 例如在Qt Quick 中,虽然QSGTexture 是公共 API,但并不存在直接暴露该对象的QQuickItem 。相反,用户必须使用 Image 项(该项既描述了纹理的来源,也描述了如何渲染它),或者编写 C++ 代码来直接操作QSGTexture 本身。而在Qt Quick 3D 中,这些资源在 QML API 中被直接暴露出来。 这是必要的,因为资源是场景状态的重要组成部分。场景中的许多对象都可以引用这些资源:例如,许多材质(Material)可以使用同一张纹理(Texture)。此外,还可以在运行时设置纹理的属性,从而直接改变纹理的采样方式,例如。

空间对象

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

资源对象

资源对象是Object3D 组件的子类。Object3D 只是QObject 的一个子类,带有用于与场景管理器配合使用的若干特殊辅助函数。资源对象确实具有父子关联关系,但这些关联主要用于资源的所有权管理。

View3D 和渲染层

就前端/后端分离而言,从用户角度看,View3D 是分离点,因为View3D 定义了要渲染的场景内容。 在Qt Quick 的空间场景图中,待渲染场景的根节点是一个Layer节点。Layer节点由View3D 通过结合View3D 的属性与SceneEnvironment 的属性来创建。当为View3D 渲染场景时,正是这个Layer节点会被传递给渲染器以渲染场景。

场景渲染

Qt Quick 3D 渲染流程图

设置渲染目标

渲染过程的第一步是确定并设置场景渲染目标。根据SceneEnvironment 中设置的属性不同,实际的渲染目标也会有所变化。 首先需要决定内容是直接渲染到窗口表面,还是渲染到离屏纹理。默认情况下,View3D 会渲染到离屏纹理。当使用后处理效果时,必须渲染到离屏纹理。

确定场景渲染目标后,将设置一些全局状态。

  • 窗口大小——若渲染至窗口
  • 视口——正在渲染的场景区域的大小
  • 剪裁矩形——视口应被裁剪到的窗口子区域
  • 清除颜色——用于清除渲染目标的颜色(如有)。

渲染准备

渲染的下一阶段是准备阶段,在此阶段,渲染器会进行内部管理,以确定给定帧中需要渲染的内容,并确保所有必要资源均已就绪且为最新状态。

准备阶段本身分为两个阶段:高级准备,即确定要渲染的内容以及所需的资源;以及低级准备,即使用 RHI 实际设置渲染管道和缓冲区,并设置主场景渲染过程的渲染依赖关系。

高级渲染准备

此阶段的目的是将空间场景图的状态提取为可用于生成渲染命令的形式。总体而言,渲染器会从单个摄像机的视角(配合一组照明状态)创建待渲染的几何体与材质组合列表。

首先需要确定所有内容的全局公共状态。如果SceneEnvironment 定义了lightProbe ,则会检查与该光探针纹理关联的环境贴图是否已加载;若未加载,则加载或生成新的环境贴图。 环境生成本身将通过一系列渲染通道,将源纹理卷积为立方体贴图。该立方体贴图将同时包含镜面反射信息和辐照度信息,后者用于材质着色。

接下来,渲染器需要确定场景中应使用哪台摄像机。如果未通过View3D 显式定义活动摄像机,则使用场景中可用的第一台摄像机。如果场景中没有摄像机,则不渲染任何内容,渲染器将终止运行。

确定摄像机后,即可计算该帧的投影矩阵。此时进行计算是因为每个可渲染对象都需要知道如何被投影。这也意味着现在可以计算出哪些可渲染对象应被渲染。 从所有可渲染对象的列表开始,我们移除所有因被禁用或完全透明而不可见的对象。 然后,如果活动摄像机启用了锥体剔除,则检查每个可渲染对象是否完全位于摄像机锥体视锥体之外;若是,则将其从可渲染对象列表中移除。

除了摄像机投影外,还会计算摄像机方向,因为这是着色代码中进行光照计算所必需的。

如果场景中存在光源节点,则将其收集到一个列表中,该列表的长度为可用光源的最大数量。如果场景中的光源节点数量超过渲染器支持的数量,则超出该限额的任何光源节点将被忽略,且不会对场景的照明产生影响。 虽然可以指定光节点的作用范围,但请注意:即使设置了作用范围,每个光源的照明状态仍会发送给所有具有照明属性的材质;不过,对于不在作用范围内的光源,其亮度将被设为 0,因此实际上这些光源不会对相应材质的照明产生影响。

现在,随着可渲染对象列表(希望)已缩短,需要更新每个项目以反映场景的当前状态。对于每个可渲染对象,我们会检查是否已加载合适的材质;若未加载,则创建一个新的材质。 材质是由着色器和渲染管线组合而成的,是生成绘制调用所必需的。此外,渲染器还会确保渲染对象所需的任何资源(例如模型上设置的几何体和纹理)均已加载。尚未加载的资源将在此处进行加载。

关于材质和特效的着色器磁盘缓存机制,请参阅“着色器的磁盘缓存”一节。

随后,可渲染对象列表会被划分为 3 个列表。

  • 不透明对象:这些对象按从前到后的顺序排序,换言之,即从距离摄像机最近的对象到距离摄像机最远的对象。这样做是为了利用硬件遮挡剔除功能,或在片段着色器中进行早期 Z 轴检测。
  • 2D 对象:这些是QtQuick 对象,由Qt Quick 渲染器进行渲染。
  • 透明对象:这些对象按从后到前的顺序排序,换言之,即从距离摄像机最远的对象到距离摄像机最近的对象。这样排序是因为透明对象需要与位于其后方的所有对象进行混合渲染。

低级渲染准备

既然本帧需要考虑的所有因素都已确定,就可以着手处理主渲染通道的基础架构和依赖关系了。此阶段的首要任务是渲染主通道所需的任何预渲染通道。

  • 渲染深度通道(DepthPass)——某些功能(如屏幕空间环境光遮蔽和阴影渲染)需要深度预渲染通道。该通道将所有不透明物体渲染到深度纹理中。
  • 渲染 SSAO 通道——屏幕空间环境光遮蔽(SSAO)通道的目标是生成环境光遮蔽纹理。该纹理随后将由材质在着色时用于加深特定区域的暗度。
  • 渲染阴影通道 - 场景中每个启用了阴影功能的光源,都会生成一个额外的阴影通道。渲染器采用两种不同的阴影渲染技术,因此根据光源类型的不同,生成的通道也会有所不同。 渲染定向光源的阴影时,系统会结合定向光源的方向和摄像机锥体大小,将场景渲染到一个 2D 遮挡纹理中。渲染点光源或聚光灯的阴影时,光源的遮挡纹理是一个立方体贴图,代表了相对于光源各面方向的遮挡贡献。
  • 渲染屏幕纹理(Render ScreenTexture)——此渲染通道仅在使用需要屏幕纹理的CustomMaterial 时才会触发,该纹理可用于折射等渲染技术。该通道的工作原理类似于深度通道,但会将所有不透明物体渲染到颜色纹理中。

在依赖渲染完成后,其余渲染阶段将被准备就绪,但不会立即渲染。 该准备工作包括将高级准备阶段收集的状态转换为图形基元,例如创建/更新统一缓冲区值、将采样器与依赖纹理关联、设置着色器资源绑定,以及创建执行绘制调用所需管线状态所涉及的所有其他操作。

场景渲染

既然繁重的准备工作已经完成,接下来轻松的部分就是执行那些构成主场景内容的命令。渲染按以下顺序进行:

  • 清除阶段——这严格来说并非一个渲染阶段,但根据SceneEnvironment 中backgroundMode的设置,此处可能发生不同的情况。 如果背景模式设置为“透明”或“颜色”,则会使用透明度或指定的颜色清空颜色缓冲区。然而,如果背景模式设置为“SkyBox”,则会运行一个渲染通道,从摄像机的视角渲染 SkyBox,同时用初始数据填充缓冲区。
  • 不透明渲染阶段——接下来将绘制所有不透明对象。这仅涉及设置管道状态,并按列表中的顺序为每个对象执行绘制命令,因为此时这些对象已经过排序。
  • 2D 渲染阶段——如果场景中存在任何 2D 对象,则会调用 `Qt Quick ` 渲染器,以生成渲染这些对象所需的渲染命令。
  • 透明渲染阶段——最后,场景中的透明对象将按照与不透明对象相同的方式逐一渲染。

至此,场景的渲染完成。

后处理

如果启用了任何后处理功能,则可以认为场景渲染器的结果是一个纹理,该纹理是后处理阶段的输入。所有后处理方法都是针对此场景输入纹理进行的额外渲染阶段。

后处理阶段的所有步骤均为可选,如果未启用任何内置功能和用户自定义效果,则场景渲染的输出结果将作为最终渲染目标使用。但请注意,tonemapping 默认处于启用状态。

后处理渲染通道图

内置后期处理

ExtendedSceneEnvironment 及其父类SceneEnvironment 提供了3D场景中最常用的效果,还包括色调映射功能——该功能用于将渲染器生成的宽动态范围(HDR)颜色值映射到0-1的低动态范围(LDR)范围内。 这些效果包括景深、光晕/光斑、镜头耀斑、暗角、色彩调整与调色、雾效以及环境光遮蔽。

后处理效果

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

该流程中的每个效果都可以由多个子通道组成,这意味着可以将内容渲染到中间缓冲区中。多通道效果的最后一个通道应输出单张纹理,其中包含后处理阶段后续步骤将使用的颜色数据。

时域和渐进式抗锯齿

可以通过在“SceneEnvironment ”中设置属性来选择性地启用“时域”和“渐进式”抗锯齿步骤。虽然严格来说它们不属于后期处理阶段,但“时域”和“渐进式”抗锯齿的实际效果是在后期处理阶段实现的。

时间抗锯齿在场景正在积极更新时执行。启用后,在渲染场景时,活动摄像机会在每帧对摄像机方向进行微小调整。随后,当前帧会与之前渲染的帧进行混合,以平滑渲染效果。

渐进式抗锯齿仅在场景未处于更新状态时执行。启用后,系统将强制进行一次更新,并在对活动摄像机方向进行微小调整的同时,渲染场景的当前状态。 系统最多会累积 8 帧画面,并根据预定义的权重将它们混合在一起。这能使静止的场景显得更加平滑,但会带来性能开销,因为每次更新都会额外渲染几帧画面。

超采样抗锯齿(SSAA)

超采样抗锯齿是一种通过“暴力”方式平滑场景的处理方法。其工作原理是先将场景渲染到比请求尺寸大若干倍的纹理中,随后将其下采样至目标尺寸。 例如,如果请求的是 2X SSAA,则场景将渲染到一个大小为预期尺寸 2 倍的纹理上,然后在此阶段进行下采样。这会对性能和资源使用产生巨大影响,因此应尽可能避免。 此外,View3D 的尺寸可能过大而无法使用此方法,因为该方法所需的纹理尺寸可能超出渲染硬件的支持范围。

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