Qt 3D 渲染帧图
Qt 3D 的渲染模块允许渲染算法完全由数据驱动。其控制数据结构被称为帧图(framegraph)。 类似于Qt 3D 的ECS(实体组件系统)允许您通过构建由实体和组件树组成的场景来定义所谓的“场景图”,帧图同样是一种树结构,但用途不同。具体而言,它是用于控制场景的渲染方式。
在渲染单帧的过程中,3D 渲染器可能会多次改变状态。这些状态变化的次数和性质不仅取决于场景中包含哪些材质(着色器、网格几何体、纹理和统一变量),还取决于您使用的是哪种高级渲染方案。
例如,使用传统的简单前向渲染方案与使用延迟渲染方法有很大不同。其他特性,如反射、阴影、多个视口以及早期Z填充阶段,都会改变渲染器在帧内需要设置的状态,以及这些状态变化需要发生的时间点。
相比之下,负责渲染《Qt Quick 2》场景的场景图渲染器(Qt Quick 2 scenegraph renderer)是通过 C++ 硬编码实现的,其操作包括原始图元的批处理,以及先渲染不透明物体、再渲染透明物体等。对于《Qt Quick 2》而言,这种设计完全可行,因为它满足了所有需求。 从上述部分示例中可以看出,鉴于可用的渲染方法多种多样,这种硬编码的渲染器对于通用 3D 场景而言,灵活性恐怕难以满足需求。或者,如果能将渲染器设计得足够灵活以涵盖所有此类情况,其性能很可能因过于通用而受到影响。 更糟糕的是,新的渲染方法正在不断被研究开发。因此,我们需要一种既灵活又可扩展,同时易于使用和维护的方法。帧图应运而生!
帧图中的每个节点都定义了渲染器用于渲染场景的配置的一部分。 节点在帧图树中的位置决定了以该节点为根的子树将在渲染管道中的何时何地成为活动配置。正如我们稍后将看到的,渲染器会遍历这棵树,以便在帧的每个时间点构建渲染算法所需的状态。
显然,如果您只想在屏幕上渲染一个简单的立方体,可能会觉得这有些小题大做。但是,一旦您开始处理稍微复杂一些的场景,它就会派上用场。对于常见情况,Qt 3D 提供了一些开箱即用的帧图示例。
我们将通过展示几个示例及其生成的帧图,来演示帧图概念的灵活性。
请注意,与由实体(Entities)和组件(Components)组成的场景图(Scenegraph)不同,帧图仅由嵌套节点组成,这些节点都是 Qt3DRender::QFrameGraphNode 的子类。这是因为帧图节点并非我们虚拟世界中的模拟对象,而是辅助信息。
我们很快将了解如何构建第一个简单的帧图,但在那之前,我们将先介绍可供您使用的帧图节点。 此外,与场景图树一样,QML 和 C++ API 是一一对应的,因此您可以选择自己最喜欢的其中一种。为了提高可读性和简洁性,本文选择了 QML API。
帧图的妙处在于,通过组合这些简单的节点类型,您可以配置渲染器以满足特定需求,而完全无需触碰任何复杂晦涩的低级 C/C++ 渲染代码。
FrameGraph 规则
为了构建一个能正常运行的帧图树,您需要了解一些关于如何遍历该树以及如何将其传递给Qt 3D 渲染器的规则。
设置帧图
应将帧图树赋值给 QRenderSettings 组件的 activeFrameGraph 属性,而该组件本身是Qt 3D 场景中根实体的组成部分。这样设置后,它便成为渲染器的活动帧图。 当然,由于这是 QML 属性绑定,因此可以在运行时动态更改活动帧图(或其部分)。例如,如果您希望对室内和室外场景使用不同的渲染方式,或者启用或禁用某些特效。
Entity {
id: sceneRoot
components: RenderSettings {
activeFrameGraph: ... // FrameGraph tree
}
}注意:activeFrameGraph 是 QML 中 FrameGraph 组件的默认属性。
Entity {
id: sceneRoot
components: RenderSettings {
... // FrameGraph tree
}
}帧图的使用方式
- Qt 3D 渲染器会对帧图树执行深度优先遍历。请注意,由于采用深度优先遍历,节点定义的顺序至关重要。
- 当渲染器到达帧图的叶节点时,它会收集从该叶节点到根节点路径上指定的所有状态。这定义了用于渲染帧中某一部分的状态。如果您对Qt 3D 的内部实现感兴趣,这组状态被称为RenderView。
- 基于 RenderView 中包含的配置,渲染器会收集场景图中所有待渲染的实体,并据此构建一组RenderCommands,将其与 RenderView 关联起来。
- RenderView 与 RenderCommands 集合的组合会被传递给 OpenGL 进行渲染。
- 当对帧图中的每个叶节点重复此过程后,帧即告完成,渲染器会调用QOpenGLContext::swapBuffers() 来显示该帧。
从本质上讲,帧图是一种用于配置Qt 3D 渲染器的数据驱动方法。正因其数据驱动的特性,我们可以在运行时更改配置,允许非 C++ 开发人员或设计师修改帧的结构,并尝试新的渲染方法,而无需编写数千行的模板代码。
帧图示例
既然您已经了解了编写帧图树时应遵循的规则,接下来我们将通过几个示例进行详细解析。
一个简单的前向渲染器
前向渲染是指以传统方式使用 OpenGL,逐个对象渲染并实时进行着色,直接渲染到后缓冲区。这与延迟渲染形成对比,后者是将渲染结果渲染到中间G 缓冲区。以下是一个可用于前向渲染的简单 FrameGraph:
Viewport {
normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
property alias camera: cameraSelector.camera
ClearBuffers {
buffers: ClearBuffers.ColorDepthBuffer
CameraSelector {
id: cameraSelector
}
}
}如您所见,该树仅有一个叶节点,总共由 3 个节点组成,如下图所示。

根据上述规则,该帧图将生成一个具有以下配置的 RenderView:
- 叶节点 -> RenderView
- 填满整个屏幕的视口(使用归一化坐标,以便轻松支持嵌套视口)
- 颜色和深度缓冲区被设置为清空
- 在暴露的 camera 属性中指定了摄像机
多个不同的 FrameGraph 树可以产生相同的渲染结果。只要从叶节点到根节点收集的状态相同,结果也会相同。最好将保持恒定时间最长的状态放置在 FrameGraph 的根节点附近,因为这样会减少叶节点的数量,从而整体上减少 RenderView 的数量。
Viewport {
normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
property alias camera: cameraSelector.camera
CameraSelector {
id: cameraSelector
ClearBuffers {
buffers: ClearBuffers.ColorDepthBuffer
}
}
}CameraSelector {
Viewport {
normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
ClearBuffers {
buffers: ClearBuffers.ColorDepthBuffer
}
}
}多视口帧图
接下来,让我们看一个稍微复杂一些的示例:它从 4 个虚拟摄像机的视角渲染场景图,并将结果显示在窗口的 4 个象限中。这是 3D CAD 或建模工具中常见的配置,也可以进行调整,用于渲染赛车游戏中的后视镜或闭路电视摄像机的显示画面。

Viewport {
id: mainViewport
normalizedRect: Qt.rect(0, 0, 1, 1)
property alias Camera: cameraSelectorTopLeftViewport.camera
property alias Camera: cameraSelectorTopRightViewport.camera
property alias Camera: cameraSelectorBottomLeftViewport.camera
property alias Camera: cameraSelectorBottomRightViewport.camera
ClearBuffers {
buffers: ClearBuffers.ColorDepthBuffer
}
Viewport {
id: topLeftViewport
normalizedRect: Qt.rect(0, 0, 0.5, 0.5)
CameraSelector { id: cameraSelectorTopLeftViewport }
}
Viewport {
id: topRightViewport
normalizedRect: Qt.rect(0.5, 0, 0.5, 0.5)
CameraSelector { id: cameraSelectorTopRightViewport }
}
Viewport {
id: bottomLeftViewport
normalizedRect: Qt.rect(0, 0.5, 0.5, 0.5)
CameraSelector { id: cameraSelectorBottomLeftViewport }
}
Viewport {
id: bottomRightViewport
normalizedRect: Qt.rect(0.5, 0.5, 0.5, 0.5)
CameraSelector { id: cameraSelectorBottomRightViewport }
}
}该树结构稍显复杂,共有 5 个叶节点。遵循与之前相同的规则,我们从帧图中构建 5 个 RenderView 对象。下图展示了前两个 RenderView 的构建过程。其余的 RenderView 与第二张图非常相似,只是涉及其他子树而已。


总体而言,创建的 RenderView 包括:
- RenderView (1)
- 定义了全屏视口
- 颜色缓冲区和深度缓冲区已设置为清空
- RenderView (2)
- 已定义全屏视口
- 子视口已定义(渲染视口将相对于其父视口进行缩放)
- 已指定相机选择器
- 渲染视图 (3)
- 已定义全屏视口
- 已定义子视口(渲染视口将相对于其父视口进行缩放)
- 已指定 CameraSelector
- 渲染视图 (4)
- 已定义全屏视口
- 已定义子视口(渲染视口将相对于其父视口进行缩放)
- 已指定 CameraSelector
- 渲染视图 (5)
- 已定义全屏视口
- 已定义子视口(渲染视口将相对于其父视口进行缩放)
- 已指定 CameraSelector
然而,在此情况下顺序至关重要。如果将 ClearBuffers 节点放在最后而不是第一位,将会导致屏幕变黑——原因很简单:所有内容在经过精心渲染后,随即就被清除了。 出于类似的原因,它也不能用作 FrameGraph 的根节点,因为这会导致每个视口都会触发清空整个屏幕的操作。
尽管 FrameGraph 的声明顺序很重要,但Qt 3D 能够并行处理每个 RenderView,因为在生成一组将在 RenderView 状态有效期间提交的 RenderCommands 时,每个 RenderView 都是相互独立的。
Qt 3D 采用基于任务的并行化方法,该方法会随着可用核心数量的增加而自然扩展。下图展示了前一个示例的情况。

RenderView 的 RenderCommands 可在多个核心上并行生成,只要我们注意在专用的 OpenGL 提交线程上按正确顺序提交 RenderView,生成的场景就会被正确渲染。
延迟渲染器
在渲染方面,就渲染器配置而言,延迟渲染与前向渲染截然不同。延迟渲染不采用逐个绘制网格并应用着色器效果进行着色的方式,而是采用两遍渲染的方法。
首先,使用相同的着色器渲染场景中的所有网格,该着色器通常会为每个片段输出至少四个值:
- 世界法线向量
- 颜色(或其他材质属性)
- 深度
- 世界位置向量
这些值将分别存储在不同的纹理中。法线、颜色、深度和位置纹理共同构成了所谓的 G-Buffer。在第一遍渲染过程中,画面上不会显示任何内容,而是将数据渲染到 G-Buffer 中,以便后续使用。
当所有网格体渲染完毕后,G-Buffer 中便会填满当前摄像机可见的所有网格体。 随后,第二遍渲染将通过从 G-Buffer 纹理中读取法线、颜色和位置值,并在全屏四边形上输出颜色,从而将场景渲染到后缓冲区并进行最终的颜色着色。
该技术的优势在于:实现复杂特效所需的强大计算能力仅在第二遍渲染时,针对摄像机实际可见的元素进行消耗。由于每块网格都使用简单的着色器进行渲染,因此第一遍渲染几乎不消耗多少处理能力。 因此,延迟渲染将着色和照明与场景中的物体数量解耦,转而与屏幕(及 G-Buffer)的分辨率耦合。由于能够使用大量动态光源(尽管会消耗额外的 GPU 内存),该技术已被广泛应用于许多游戏中。
Viewport {
id: root
normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
property GBuffer gBuffer
property alias camera: sceneCameraSelector.camera
property alias sceneLayer: sceneLayerFilter.layers
property alias screenQuadLayer: screenQuadLayerFilter.layers
RenderSurfaceSelector {
CameraSelector {
id: sceneCameraSelector
// Fill G-Buffer
LayerFilter {
id: sceneLayerFilter
RenderTargetSelector {
id: gBufferTargetSelector
target: gBuffer
ClearBuffers {
buffers: ClearBuffers.ColorDepthBuffer
RenderPassFilter {
id: geometryPass
matchAny: FilterKey {
name: "pass"
value: "geometry"
}
}
}
}
}
TechniqueFilter {
parameters: [
Parameter { name: "color"; value: gBuffer.color },
Parameter { name: "position"; value: gBuffer.position },
Parameter { name: "normal"; value: gBuffer.normal },
Parameter { name: "depth"; value: gBuffer.depth }
]
RenderStateSet {
// Render FullScreen Quad
renderStates: [
BlendEquation { blendFunction: BlendEquation.Add },
BlendEquationArguments {
sourceRgb: BlendEquationArguments.SourceAlpha
destinationRgb: BlendEquationArguments.DestinationColor
}
]
LayerFilter {
id: screenQuadLayerFilter
ClearBuffers {
buffers: ClearBuffers.ColorDepthBuffer
RenderPassFilter {
matchAny: FilterKey {
name: "pass"
value: "final"
}
parameters: Parameter {
name: "winSize"
value: Qt.size(1024, 768)
}
}
}
}
}
}
}
}
}(以上代码改编自qt3d/tests/manual/deferred-renderer-qml。)
从图形上看,生成的帧图如下所示:

生成的 RenderView 如下:
- RenderView (1)
- 指定应使用哪台摄像机
- 定义一个填满整个屏幕的视口
- 选择图层组件 sceneLayer 下的所有实体
- 将
gBuffer设置为活动渲染目标 - 清除当前绑定渲染目标(
gBuffer)上的颜色和深度 - 仅选择场景中材质(Material)和技术(Technique)与 RenderPassFilter 中的注释相匹配的实体
- 渲染视图 (2)
- 定义一个填满整个屏幕的视口
- 选择图层组件 screenQuadLayer 下的所有实体
- 清除当前绑定帧缓冲区(屏幕)上的颜色和深度缓冲区
- 仅选择场景中材质(Material)和技术(Technique)与 RenderPassFilter 中的注释相匹配的实体
帧图的其他优势
由于帧图(FrameGraph)树完全由数据驱动,且可在运行时动态修改,因此您可以:
- 为不同的平台和硬件准备不同的帧图树,并在运行时选择最合适的
- 在场景中轻松添加并启用可视化调试
- 根据场景特定区域需要渲染内容的性质,使用不同的 FrameGraph 树
- 实现新的渲染技术,而无需修改Qt 3D 的内部实现
结论
我们已经介绍了FrameGraph及其组成节点类型。 随后,我们通过几个示例来阐释 FrameGraph 的构建规则,以及Qt 3D 引擎在后台如何使用 FrameGraph。至此,您应该已经对 FrameGraph 及其使用方法(例如为前向渲染器添加早期 z 填充通道)有了相当全面的了解。 此外,请务必牢记:FrameGraph 是一项供您使用的工具,它能让您摆脱对Qt 3D 默认提供的渲染器和材质的依赖。
© 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.