Advertisement

C++学习(一五九)Qt的场景图Scene Graph

阅读量:

更适合地表示场景树的本质属性,并非基于传统的图形化表示框架。在QML场景中构建的Qt Quick项目将专注于生成用于填充QSGNode实例树的结构

场景图是在Qt Quick 2.0中被引入的一种工具,在构建可视化界面时起到核心作用。它适用于所有基于QMake开发的项目,并通过提供统一的接口简化了图形元素的绘制过程。其默认实现与基于OpenGL的高效渲染架构紧密相连,从而实现了高质量的画面显示效果。

Qt和OSG中的场景图在组织架构上存在相似之处。具体而言,它们均是由不同节点按照特定关系构成。值得注意的是,在节点数量方面,OSG的表现更为丰富,并且每个节点还附加了相应的渲染状态信息。在显示效果方面,Qt直接处理其场景图进行显示操作;而OSG则将场景图转换为渲染树后再执行绘制操作(以减少对渲染状态频繁更改的影响)。

QT的场景图是基于界面元素的位置和透明度等信息进行构造形成的;而OSG则采用了节点制的方法来生成图像。换言之,在使用时你无需自行搭建QT的画面架构;但可以在OSG中自由配置并操作图像结构

例如,在一个用户的界面设计中包含十个项目的列表展示功能时(即每个项目都具有特定的背景色、图标以及文字说明),传统的方法通常需要频繁地进行绘图操作(约30次),这会导致大量的不必要的绘图操作(约30次)以及类似的频繁状态更新操作(约30次)。相比之下,在采用场景重组策略的情况下(即通过场景图的方式重组基本元素来进行绘制),可以在单个渲染阶段一次性完成所有背景元素的绘制(即首先处理所有的背景信息),接着绘制所有的图标元素(即依次添加各个图标),最后处理所有的文本信息(即逐步加载文字内容)。这种优化策略能够显著减少整个渲染流程所需的操作次数(从约30次减少至仅仅3个关键步骤)。同时,在图形渲染效率方面也取得了明显的提升效果:通过减少批量处理任务以及状态更新频率的优化策略……能够显著提升相关硬件系统的性能水平。

场景图由QQuickWindow类负责生成和显示,并以图形形式呈现给用户界面。定制化的Item类可以通过调用QQuickItem::updatePaintNode()函数来将其中的图形元素添加到展示窗口中。

场景图是Item场景的视觉化表示其结构设计具备高度独立性其涵盖所有必要的渲染信息当设置完成后能够完全脱离当前项目的状态进行操作与渲染在许多平台上该显示结构被优化地在专用渲染线程上执行相应的任务即使是在GUI线程准备下一帧状态的时候

场景图的结构

场景图由多个固定配置的分类模块构成,并每个分类模块均具备独特功能属性。然而其被称为场景图的是实质上为一种层级结构化的数据模型——节点树体系。这种树状架构基于QML框架下的QQuickItem对象进行构建,在系统内部则被赋予负责解析与呈现相应图形内容的责任权。值得注意的是这些独立节点并不拥有主动发起绘图操作的能力而是仅存储着相关的绘制指令与显示逻辑。

尽管节点树主要基于现有的Qt Quick QML类型的内部架构构建, 同时允许用户提供自定义内容的完整子树结构, 其中特别包括用于表示三维模型的子树。

节点

From the user's perspective, the most critical component is QSGGeometryNode, which serves as the foundation for creating custom graphical interfaces by defining its geometric shape and material properties. The tool employs QSGGeometry to generate precise geometric forms and mesh structures, supporting elements such as straight lines, rectangles, polygons, disjointed rectangular regions, and intricate three-dimensional meshes. Finally, the material parameters determine how pixels within a selected region are filled during rendering processes.

每个节点都可以包含任意多个子节点,并且该系统会生成相应的几何图形以便它们按照从子到父的顺序排列;同时确保父级节点则位于所有子级节点之前。

常见的节点有:

QSGClipNode 在场景图中实现裁剪功能的节点
QSGGeometryNode 用于场景图中的所有渲染内容的节点
QSGNode 场景图中所有节点的基类的节点
QSGOpacityNode 用于更改节点的不透明度的节点
QSGTransformNode 在场景图中实现变换的节点
QSGRenderNode 表示一组针对场景所使用的图形API的自定义渲染命令。

在继承于子类QQuickItem的基础上,并调用updatePaintNode()方法同时设置QQuickItem::ItemHasContents标志以实现将自定义节点加入到场景图中

核心在于,在本机图形操作(包括OpenGL、Vulkan和Metal等技术)及其与场景图交互方面实施特定处理时必须要单独在渲染线程中执行以确保图像质量不受影响,并且这些操作通常会集中在updatePaintNode()函数被调用的过程中。根据经验法则,在QQuickItem::updatePaintNode()函数内部应优先使用带有"QSG"前缀的类来实现相关功能。

处理过程

节点包含一个虚拟QSGNode::preprocess()函数,在呈现场景图之前会被调用以负责处理节点需要渲染的内容。通过设置标志QSGNode::UsePreprocess并重新编写QSGNode::preprocess()函数可以在子类中完成最终准备工作。例如,在比例因子正确细节级别上对贝塞尔曲线进行划分,并更新纹理的部分内容。

节点的所有权

节点的所有权归创建者或场景图通过设置'OwnedByParent'标志明确无误地完成。一般情况下,赋予场景图所有权是可行的选择,因为它有助于简化其在GUI线程之外时的操作处理。

材质

该材质详细说明了如何在QSGGeometryNode中填充几何图形的内部区域。该封装包含用于图形管线顶点和片段阶段的应用着色器功能,并提供了高度定制化的能力。然而,在大多数Qt Quick项目中,默认配置通常较为简单,默认情况下通常仅依赖纯色或纹理填充等基本元素进行处理。

个人用户若希望仅设置自定义阴影于QML Item类型上,则可采用ShaderEffect类型的解决方案。

以下是材质类别的完整列表:

QSGFlatColorMaterial 在场景图中渲染纯色几何的便捷方法
QSGMaterial 封装着色器程序的渲染状态
QSGMaterialRhiShader 表示独立于图形API的着色器程序
QSGMaterialShader 表示渲染器中的OpenGL着色器程序
QSGMaterialType 与QSGMaterial结合用作唯一类型令牌
QSGOpaqueTextureMaterial 在场景图中渲染纹理几何的便捷方法
QSGTextureMaterial 在场景图中渲染纹理几何的便捷方法
QSGVertexColorMaterial Convenient way of rendering per-vertex colored geometry in the scene graph

便利节点

该场景图API属于基础级别,并非专门设计用于提高便利性。为了实现自定义几何体和材质的开发,在即使是基础几何体和材质的情况下也需要编写大量代码。由此可见,该API提供了一些辅助功能库以方便让大部分自定义组件的操作更加便捷。

该实例基于纯色材质定义了矩形几何

QSGSimpleTextureNode-QSGGeometryNode继承自QSGGeometryNode类,该对象通过纹理材质描述了矩形的几何形状。

场景图与渲染

场景图的显示位于QQuickWindow类的内部区域,并未提供任何公共接口供外部程序访问。然而,在这个显示管道中存在一些特定区域可以让开发者嵌入自定义的应用代码。通过直接调用被场景图所使用的图形API(如OpenGL、Vulkan和Metal等),开发者可以实现添加定制化场景元素的功能或者插入任意级别的渲染指令。该功能由统一的渲染循环机制进行管理。

存在三种渲染循环模式:基础型、窗口型及线程型。其中基础型与窗口型为单线程模式,在专用主线程上执行场景图绘制任务。 Qt会尝试根据当前平台及其可能支持的图形驱动程序来选择合适的循环模式。如果对此不感到满意或者出于测试目的,则可以通过环境变量QSG_RENDER LOOP指定所需的循环模式。为了验证采用了哪种渲染循环模式,请启用qt.scenegraph.general日志记录功能。

该线程及Windows渲染循环依赖于相应的图形API实现来执行速率控制。具体而言,在OpenGL情况下,默认情况下请求交换的时间间隔被设定为1。然而某些图形驱动程序设计上提供了可选功能,使用户可以选择关闭此参数而不影响系统运行。但未对Qt相关的请求进行处理。当避免阻塞交换缓冲区或其他关键区域的操作时,在动画播放期间渲染循环可能会超速运行至CPU满负荷运转。如果系统无法通过VGS同步机制来限制动画速度,则应优先选择基本渲染循环模式来确保性能稳定。

基于线程的渲染循环

通常在许多配置中

下面介绍如何使用线程渲染循环以及OpenGL渲染帧的具体步骤。除了在OpenGL上下文中有一些特别需要注意的地方外,在其他图形API的具体步骤也是一致的。

在QML场景发生变化时会触发QQuickItem::update()函数的调用。这可能是因为动画效果或者用户的直接输入所引发的事件会被发布到渲染线程,并用于启动新的渲染帧

2、渲染线程准备绘制新帧。

当渲染线程在生成新的帧时,在此期间(GUI线程)将执行QQuickItem::updatePolish()以优化项目的细节和性能,并在此之后完成所有必要的修饰工作。

4、阻塞GUI线程。

该程序会触发一个名为beforeSynchronizing的信号。应用程序可以使用Qt::DirectConnection建立一个对该信号的直接连接,在此之前完成任何必要的初始化工作。

同步QML状态至场景图中。
是在自上一帧以来已更改的所有项目上调用该函数来实现的。
这种交互是基于单一节点的。

7、释放GUI线程。

8、渲染场景图

8.1、触发QQuickWindow::beforeRendering()信号。该程序可通过该信号的直接接口实现与外部系统的交互。当应用需要调用自定义图形API时,在接收该信号后会对其进行可视化展示并合理地叠加到其所在QML场景中。

8.2、被指定为QSGNode :: UsePreprocess项目的项目会被调用其QSGNode :: preprocess()子函数。

8.3、渲染器处理节点。

8.4、渲染器生成状态并记录使用中的图形API的绘制调用。

8.5、触发QQuickWindow::afterRendering()事件。应用程序可通过使用Qt::DirectConnection的方式对该事件进行直接响应(即进行直接连接),从而发射自定义图形API调用,并将这些调用以可视化的形式叠加到QML场景中。

第8.6节现已准备好并就绪。
或者切换缓冲区(OpenGL),另外一种方法是捕获当前指令并发送至图形流水队列(Vulkan与Metal兼容)。QQuickWindow::frameSwapped()已被触发。

9、在渲染线程正在渲染时,GUI可以自由地进行动画,处理事件等。

通常情况下[线程渲染器]可用于包含opengl32.dll的操作系统环境[如Windows]以及不依赖于Mesa llvmpipe[如Linux]等多款操作系统上运行[而针对移动设备][嵌入式Linux系统以及Vulkan架构设备等则依赖于特定条件的支持]。需要注意的是由于可能会有所更改因此建议谨慎操作[而通过环境变量配置QSG_RENDER_LOOP = threaded则可以强制启用此功能以确保稳定运行]

有关frameSwapped信号

一旦帧处于队列等待呈现的状态时,则会触发此信号。启用垂直同步机制后,在连续动画过程中仅限于每次vsync间隔会发送一次信号;该信号则源自于场景图形渲染线程的核心单元负责发送。

渲染线程的渲染代码:

复制代码
 qtdeclarative\src\quick\scenegraph\qsgthreadedrenderloop.cpp

    
 void QSGRenderThread::syncAndRender()
    
 {
    
     bool profileFrames = QSG_LOG_TIME_RENDERLOOP().isDebugEnabled();
    
     if (profileFrames) {
    
     sinceLastTime = threadTimer.nsecsElapsed();
    
     threadTimer.start();
    
     }
    
     Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphRenderLoopFrame);
    
  
    
     QElapsedTimer waitTimer;
    
     waitTimer.start();
    
  
    
     qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "syncAndRender()");
    
  
    
     syncResultedInChanges = false;
    
     QQuickWindowPrivate *d = QQuickWindowPrivate::get(window);
    
  
    
     bool repaintRequested = (pendingUpdate & RepaintRequest) || d->customRenderStage;
    
     bool syncRequested = pendingUpdate & SyncRequest;
    
     bool exposeRequested = (pendingUpdate & ExposeRequest) == ExposeRequest;
    
     pendingUpdate = 0;
    
  
    
     if (syncRequested) {
    
     qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- updatePending, doing sync");
    
     sync(exposeRequested);//从这里调用各个QQuickItem(包括其继承类)的updatePaintNode函数
    
     }
    
 #ifndef QSG_NO_RENDER_TIMING
    
     if (profileFrames)
    
     syncTime = threadTimer.nsecsElapsed();
    
 #endif
    
     Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
    
                           QQuickProfiler::SceneGraphRenderLoopSync);
    
  
    
     if (!syncResultedInChanges && !repaintRequested && sgrc->isValid()) {
    
     qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- no changes, render aborted");
    
     int waitTime = vsyncDelta - (int) waitTimer.elapsed();
    
     if (waitTime > 0)
    
         msleep(waitTime);
    
     return;
    
     }
    
  
    
     qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- rendering started");
    
  
    
  
    
     if (animatorDriver->isRunning()) {
    
     d->animationController->lock();
    
     animatorDriver->advance();
    
     d->animationController->unlock();
    
     }
    
  
    
     bool current = false;
    
     if (d->renderer && windowSize.width() > 0 && windowSize.height() > 0)
    
     current = gl->makeCurrent(window);
    
     // Check for context loss.
    
     if (!current && !gl->isValid()) {
    
     // Cannot do anything here because gui is not locked. Request a new
    
     // sync+render round on the gui thread and let the sync handle it.
    
     QCoreApplication::postEvent(window, new QEvent(QEvent::Type(QQuickWindowPrivate::FullUpdateRequest)));
    
     }
    
     if (current) {
    
     d->renderSceneGraph(windowSize);//执行场景图的渲染操作,本质上是调用opengl命令
    
     if (profileFrames)
    
         renderTime = threadTimer.nsecsElapsed();
    
     Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
    
                               QQuickProfiler::SceneGraphRenderLoopRender);
    
     if (!d->customRenderStage || !d->customRenderStage->swap())
    
         gl->swapBuffers(window);//交换前后缓冲区,将渲染的内容显示出来
    
     d->fireFrameSwapped();
    
     } else {
    
     Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphRenderLoopFrame,
    
                             QQuickProfiler::SceneGraphRenderLoopSync, 1);
    
     qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- window not ready, skipping render");
    
     }
    
  
    
     qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- rendering done");
    
  
    
     // Though it would be more correct to put this block directly after
    
     // fireFrameSwapped in the if (current) branch above, we don't do
    
     // that to avoid blocking the GUI thread in the case where it
    
     // has started rendering with a bad window, causing makeCurrent to
    
     // fail or if the window has a bad size.
    
     if (exposeRequested) {
    
     qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- wake Gui after initial expose");
    
     waitCondition.wakeOne();
    
     mutex.unlock();
    
     }
    
  
    
     qCDebug(QSG_LOG_TIME_RENDERLOOP,
    
         "Frame rendered with 'threaded' renderloop in %dms, sync=%d, render=%d, swap=%d - (on render thread)",
    
         int(threadTimer.elapsed()),
    
         int((syncTime/1000000)),
    
         int((renderTime - syncTime) / 1000000),
    
         int(threadTimer.elapsed() - renderTime / 1000000));
    
  
    
  
    
     Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame,
    
                        QQuickProfiler::SceneGraphRenderLoopSwap);
    
 }

非线程的渲染循环(basic或windows)

通常在Windows系统中(当使用ANGLE或非默认的OpenGL 3.2实现时)会采用OpenGL。 macOS系统通常应用OpenGL。在特定Linux系统(支持特定驱动程序)中也会采用。为了避免测试不足风险的技术手段是必要的。此外还需要考虑到那些无法应用于线程渲染场景的实现如ANGLE和Mesa llvmpipe等因此应避免在这些情况下采用线程渲染循环。

在macOS平台和OpenGL环境下,默认情况下,在 macOS 10.14 系列操作系统及其以上版本中应用 XCode 10 或更高版本进行编译时,默认无法启用线程渲染循环这一特性存在。这是因为这样做会迫使编译器选择在 macOS 10.14 系列系统上基于视图层的UI框架来进行图形呈现。如果需要实现线程渲染功能,则必须配置 Xcode 9 或更低版本(即 Xcode 9对应的是 macOS 10.13 SDK)来禁用对图形API的支持,在这种情况下编译器才会允许并默认启用线程渲染循环。而 Metal API对此不做限制

通常情况下,默认 Windows系统会在支持 ANGLE 的 Windows 系统上采用非线程渲染模式;而在需要执行非脚本任务时,则会采用 basic 方式处理所有其他平台。

即使采用异步渲染循环时,为了实现一致性和高效性,必须按照同步渲染机制来编程逻辑。若不如此,则会导致难以跨平台运行。

以下是非线程渲染器中帧渲染序列的简化图示。

使用QQuickRenderControl自定义渲染控制

在采用QQuickRenderControl的情况下,则避免内置的渲染机制,并将负责渲染循环的任务转移至应用层面。替代方案是让应用程序在适当时机执行抛光、同步以及渲染流程中的相关操作步骤;这样的做法既可应用于线性事件序列也可用于非线性事件序列。

混合场景图和本机图形API

场景图支持两种方法来整合应用程序提供的图形指令:第一种方法是通过直接发送OpenGL、Vulkan和Metal等指令实现;第二种方法则是在场景图中创建纹理化的节点来进行数据整合。

通过将QSGRendererInterface绑定到QQuickWindow :: beforeRendering()和QQuickWindow :: afterRendering()这两个信号上,应用程序能够在场景图渲染完成后的统一环境中直接发起OpenGL操作。利用Vulkan或Metal等API,则可以通过查询本机对象获取场景图的命令缓冲区,并根据需要向该缓冲区提交命令。这些信号名称提示用户可以在Qt Quick场景中或上方渲染内容。这种集成方式的优势在于无需额外的帧缓冲区、内存空间或其他资源消耗,并且避免了可能耗时的纹理化步骤。然而其缺点在于只能由Qt Quick决定何时触发这些信号以进行 OpenGL 绘制操作,在性能优化方面存在一定的局限性。

另一种方法(仅限于OpenGL)是在程序中创建一个QQuickFramebufferObject对象,并将绘制结果存储在其中;随后将绘制结果作为纹理加载到游戏引擎中以供展示在虚拟环境中使用

即便QQuickFramebufferObject目前不支持,在除OpenGL之外的其他图形API上仍可采取此方法。“场景图-金属纹理导入”这一案例通过直接调用基础API创建并呈现纹理实例,并将其打包后整合至自定义的QQuickItem及其Qt Quick场景中加以利用。值得注意的是,“ Metal ”技术在此案例中被采用;然而这一技术方案同样适用于所有其他的图形API。

注意:当将OpenGL相关内容与场景图形渲染进行混合处理时,请确保应用程序应避免让OpenGL上下文进入缓冲区绑定状态,并启用了属性如z缓存或其他相关属性。这种做法可能会引发不可预知的问题。

请注意:自定义渲染代码应当认识到它们处于线程环境中运行,并非仅仅在应用程序的GUI主线程上进行。

使用QPainter的自定义Item

QQuickItem 包含了一个名为 QQuickPaintedItem 的子类。该子类支持用户通过 QPainter 来绘制图像内容。

提示:为提升效率,在处理QQuickPaintedItem时可采用软件实现的多边形渲染或基于OpenGL的FBO技术以间接呈现2D图像。具体来说,这一过程需分两步完成。其中第一步是进行面元素格化处理随后进行着色运算。若采用场景图API相关接口则可显著提升性能。

日志功能

场景图包含多种日志记录类别。不仅有助于 Qt 贡献者,同时还可以用于跟踪性能问题以及错误。

qt.scenegraph.time.texture-记录进行纹理上传所花费的时间

qt.scenegraph.time.compilation-记录进行着色器编译所花费的时间

qt.scenegraph.time.renderer-记录渲染器各个步骤所花费的时间

qt.scenegraph.time.renderloop-记录渲染循环各个步骤所花费的时间

qt.scenegraph.time.glyph-记录准备距离场字形所花费的时间

qt.scenegraph.general-涵盖场景图及其相关图形堆栈各组成部分的基本信息

qt.scenegraph.renderloop-记录渲染过程中所涉及的各种环节的详细日志。该方案设计专为此类场景而生,并主要用于帮助 Qt 开发人员更高效地完成相关工作。

在旧版本中即可使用QSG_INFO环境变量。若要使其生效,请将其配置为非零值以激活qt.scenegraph.general类别。

注意:当遇到与图形相关的问题时(如不确定所使用的渲染循环或图形API),请确保始终启用了Qt scenigraph.general和Qt RHI库(或已设置QSG_INFO=1)的情况后再启动应用程序。这样做会使得在应用初始化阶段会输出一些基础信息到调试日志中。

除了场景图之外,在系统架构中还设有适应层这一机制。此机制采用一种独特的技术方案用于实现硬件特定的适配需求。此乃一项未曾对外公布且专为内部及专用插件设计的独特技术,并可使硬件适配组充分利用其 hardware 资源。

自定义的纹理:特别地,在QQuickWindow :: createTextureFromImage这一实现中引入了大量基于该图像类型和其边界图像类型的纹理,并对这些纹理的内部表示形式进行了详细优化。

自定义渲染器基于适配层决定了插件能够自主规划遍历与渲染场景图的过程。这使得我们能够在特定硬件上优化渲染算法,并且借助性能提升的技术实现功能扩展。

许多默认QML类型的自定义场景图实现,包括其文本和字体渲染。

定制化的动画驱动程序可支持通过硬件级的显示设备接口与垂直刷新同步工作,从而实现流畅的画面呈现。

自定义渲染循环:可以更好地控制QML如何处理多个窗口。

全部评论 (0)

还没有任何评论哟~