Qt 3D渲染帧图

Qt 3D渲染方面允许渲染算法完全由数据驱动。控制数据结构被称为帧图。类似于Qt 3D ECS(实体组件系统)允许您通过构建一个由实体和组件构成的场景树来定义所谓的场景图,帧图也是一个树结构,但它用于不同的目的。即控制如何渲染场景。

在渲染单个帧的过程中,3D渲染器可能会多次改变状态。这些状态变化的数量和性质不仅取决于场景中包含哪些材料(着色器、网格几何形状、纹理和均匀变量),还取决于您正在使用的高级渲染方案。

例如,使用传统的简单前向渲染方案与使用延迟渲染方法非常不同。其他功能,如反射、阴影、多个视口和早期z填充操作,都会改变渲染器需要在帧的整个过程中设置的状态以及何时需要这些状态变化。

作为比较,负责绘制Qt Quick 2场景的Qt Quick 2场景图渲染器是用C++硬编码的,以执行诸如批处理和管理不透明项以在下透明项之后渲染等方法。对于Qt Quick 2来说,这是完全可以接受的,因为它涵盖了所有需求。如图中所示的一些示例所示,这种硬编码的渲染器可能不足以通用于通用的3D场景,考虑到有如此多的渲染方法可用。或者如果可以将渲染器变得足够灵活以覆盖所有这些情况,其性能可能会因为过于通用而受到影响。更糟糕的是,还在不断研究新的渲染方法。因此,我们需要一种既灵活又可扩展,同时使用和维护简单的方法。这时,帧图就出现了!

帧图中的每个节点定义了渲染器渲染场景时将使用的配置的一部分。节点在帧图树中的位置决定了以该节点为根的子树的何时何地将成为渲染管道中的活动配置。正如我们将稍后看到的,渲染器将遍历此树以在帧的每个点构建渲染算法所需的状态。

显然,如果您只想在屏幕上渲染一个简单的立方体,您可能会认为这有点过度。然而,一旦您想要开始创建稍微复杂一些的场景,这就会很有帮助。对于常用情况,Qt 3D提供了一些现成的示例帧图,可以直接使用。

我们将通过提供一些示例及其结果帧图来展示帧图概念的可扩展性。

请注意,与由实体和组件组成的场景图不同,帧图仅由嵌套节点组成,它们都是Qt3DRender::QFrameGraphNode的子类。这是因为帧图节点不是我们虚拟世界中的模拟对象,而是支持信息。

我们将很快介绍如何构造我们的第一个简单的framegraph,但在那之前,我们将介绍您可以使用的framegraph节点。同样,就像Scenegraph树一样,QML和C++ API是一对一的匹配,因此您可以优先选择您最喜欢的一个。为了确保可读性和简洁性,本文选择了QML API。

framegraph的美丽之处在于,通过结合这些简单的节点类型,您可以配置渲染器以适应您的特定需求,而无需修改任何复杂的底层C/C++渲染代码。

FrameGraph规则

为了构造一个正确运行的framegraph树,您需要了解一些关于如何遍历它以及如何将其传递给Qt 3D渲染器的规则。

设置Framegraph

FrameGraph树应分配给QRenderSettings组件的activeFrameGraph属性,该组件本身是Qt 3D场景中根实体的一个组件。这使得它成为渲染器的活动framegraph。当然,由于这是一个QML属性绑定,因此可以在运行时动态更改活动framegraph(或其部分)。例如,如果您想为室内和室外场景使用不同的渲染方法或启用或禁用某些特殊效果。

Entity {
    id: sceneRoot
    components: RenderSettings {
         activeFrameGraph: ... // FrameGraph tree
    }
}

注意:activeFrameGraph是QML中FrameGraph组件的默认属性。

Entity {
    id: sceneRoot
    components: RenderSettings {
         ... // FrameGraph tree
    }
}

Framegraph的使用方法

  • Qt 3D渲染器对framegraph树进行深度优先遍历。请注意,由于遍历是深度优先的,所以定义节点的顺序很重要
  • 当渲染器到达framegraph的叶节点时,它收集了从叶节点到根节点的路径上指定的所有状态。这定义了用于渲染帧部分的状态。如果您对Qt 3D的内部结构感兴趣,这种状态集合被称为RenderView
  • 给定的RenderView中的配置,渲染器收集要渲染的Scenegraph中的所有实体的集合并从中构建一组RenderCommands,并将其与RenderView相关联。
  • 将RenderView和一组RenderCommands传递给OpenGL进行提交。
  • 当对framegraph中的每个叶节点重复此操作时,帧就完成了,渲染器调用QOpenGLContext::swapBuffers()来显示帧。

在本质上,framegraph是一种数据驱动的配置Qt 3D渲染器的方法。由于其数据驱动特性,我们可以在运行时更改配置,允许非C++的开发者或设计师更改帧的结构,并尝试新的渲染方法,而无需编写数千行模板代码。

Framegraph示例

现在您知道了编写framegraph树时应遵守的规则,我们将分析一些示例并将其分解。

简单的前向渲染器

前向渲染是在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个节点,如以下图表所示。

根据上面定义的规则,这个framegraph树产生了一个具有以下配置的单个RenderView

  • 叶子节点 -> 渲染视图
    • 整个屏幕都填充的视口(使用归一化坐标,以便于支持嵌套视口)
    • 颜色和深度缓冲区已设置清除
    • 在公开的摄像头属性中指定的摄像头

多个不同的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
           }
      }
}

多视口FrameGraph

让我们进一步探讨一个稍复杂的示例,从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个叶子节点。按照之前的规则,我们从FrameGraph构建了5个RenderView对象。以下图表显示了前两个RenderView的构建。其余的RenderView与第二个图非常相似,只是有其他的子树。

具体来说,创建的RenderViews为:

  • RenderView (1)
    • 全屏视口定义
    • 颜色和深度缓冲区已设置清除
  • RenderView (2)
    • 全屏视口定义
    • 子视口定义(渲染视口将相对于其父对象进行缩放)
    • CameraSelector指定
  • RenderView (3)
    • 全屏视口定义
    • 子视口定义(渲染视口将相对于其父对象进行缩放)
    • CameraSelector指定
  • RenderView (4)
    • 全屏视口定义
    • 子视口定义(渲染视口将相对于其父对象进行缩放)
    • CameraSelector指定
  • RenderView (5)
    • 全屏视口定义
    • 子视口定义(渲染视口将相对于其父对象进行缩放)
    • CameraSelector指定

然而,在这种情况下,顺序很重要。如果ClearBuffers节点在第一个而不是最后一个,这将导致一个黑色屏幕,因为所有的渲染物都会在细致渲染后被清除。出于类似的原因,它不能作为FrameGraph的根使用,因为这会为我们的每个视口调用清除整个屏幕。

尽管FrameGraph的声明顺序很重要,但Qt 3D能够并行处理每个RenderView,因为每个RenderView在生成要提交的Render命令集时都与其他RenderView独立

Qt 3D使用基于任务并行方法,该方法能够自然地根据可用核心数量进行扩展。以下图表显示了前面示例的情况。

可以在多个核心上并行生成RenderView的Render命令,只要我们注意在专用的OpenGL提交线程上以正确的顺序提交RenderView,生成的场景就可以正确渲染。

延迟渲染器

在渲染方面,相对于正向渲染,延迟渲染在渲染器配置方面是一个不同的实体。与逐个绘图和应用着色器效果来着色网格不同,延迟渲染采用了一种双渲染通道的方法。

首先,使用相同的着色器绘制场景中的所有网格,该着色器通常对于每个片段至少输出四个值

  • 世界法向量
  • 颜色(或一些其他材料属性)
  • 深度
  • 世界位置向量

这些值中的每一个都会存储在一个纹理中。法线、颜色、深度和位置纹理形成所谓的G-Buffer。第一次渲染阶段不会在屏幕上显示任何内容,而是绘制到G-Buffer中,以便以后使用。

一旦所有网格都绘制完毕,G-缓冲区将填充当前相机可以看到的所有网格。然后,第二个渲染过程用来渲染场景到后缓冲区,通过读取G-缓冲区纹理中的法线、颜色和位置值,将颜色输出到一个全屏四边形上,并应用最终的颜色着色。

该技术的优势在于,复杂效果所需的强大计算能力仅在第二个渲染过程中使用,而此时仅对相机实际上看到的元素进行渲染。第一个渲染过程中每个网格都会使用简单的着色器进行渲染,因此不会耗费太多处理能力。因此,延迟渲染将着色和光照与场景中的对象数量解耦,而是将其与屏幕分辨率(和G-缓冲区)相关联。这是一个在许多游戏中使用的技巧,因为它能够以额外的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。)

从图形上讲,生成的framegraph看起来像

并且生成的RenderViews是

  • RenderView (1)
    • 指定应使用哪个相机
    • 定义一个填充整个屏幕的视口
    • 选择用于层组件sceneLayer的所有实体
    • 将gBuffer作为活动渲染目标设置
    • 清除当前绑定渲染目标(gBuffer)的颜色和深度
    • 选择场景中具有与RenderPassFilter 中注释匹配的材质和技术的实体
  • RenderView (2)
    • 定义一个填充整个屏幕的视口
    • 选择用于层组件screenQuadLayer的所有实体
    • 清除当前绑定的帧缓冲区(屏幕)的颜色和深度缓冲区
    • 选择场景中具有与RenderPassFilter 中注释匹配的材质和技术的实体

framegraph的其它好处

由于FrameGraph树完全由数据驱动,并且可以在运行时动态修改,因此您可以使用以下方法

  • 为不同的平台和硬件设计不同的framegraph树,并在运行时选择最合适的一个
  • 轻松地在一个场景中添加和启用可视化调试
  • 根据特定场景部分的渲染需求使用不同的FrameGraph树
  • 在不修改Qt 3D内部的情况下实现新的渲染技术

结论

我们介绍了FrameGraph及其组成节点类型。然后我们讨论了一些示例,以说明framegraph构建规则以及Qt 3D引擎如何在幕后使用framegraph。到此为止,您应该对FrameGraph及其使用方法有了很好的了解(可能包括向前渲染器添加早期Z填充过程)。另外,您应该始终牢记,FrameGraph是一个供您使用的工具,因此您不必局限于Qt 3D开箱即提供的渲染器和材质。

© 2024 The Qt Company Ltd. 本文档中包含的文档贡献归其各自所有者所有。此处提供的文档根据自由软件基金会发布的GNU自由文档许可证版本1.3的条款进行许可。Qt及其相应标志是The Qt Company Ltd.在芬兰以及全球其他国家的商标。所有其他商标均为各自所有者的财产。