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

2022-11-15 11:29:35 浏览数 (1)

叫场景树更合适,本质不是图。QML场景中的Qt Quick项目将填充QSGNode实例树。

场景图是Qt Quick 2.0引入的,建立在要绘制的内容是已知的基础上。所有QML项目均使用场景图进行渲染,场景图的默认实现是与OpenGL紧密相关的低级高性能渲染堆栈。

qt的场景图和osg的场景图的组织上有些类似,都是不同节点通过一定关系构建的,但是osg的场景节点更多些,并且还关联了渲染状态。在渲染方面,qt是直接对场景图进行渲染,osg是将场景图转换为渲染树再进行渲染(避免渲染状态的频繁切换)。

qt的场景图是根据界面元素的位置、透明等信息构建出来的,而osg的场景图是直接利用节点构建出来的。也就是用户不直接参与qt场景图的构建,但是直接参与osg场景图的构建。

例如,假设用户界面包含十个项目的列表,其中每个项目都有背景色,图标和文本。使用传统的绘图技术,这将导致30次绘图调用和类似数量的状态更改。另一方面,场景图可以重组原始图元以进行渲染,以便在一次调用中绘制所有背景,然后绘制所有图标,然后绘制所有文本,从而将绘制调用的总数减少到仅3个。批处理和状态更改减少这样可以大大提高某些硬件的性能。

场景图由QQuickWindow类管理和呈现,自定义Item类型可以通过调用QQuickItem :: updatePaintNode()将其图形基元添加到场景图中。

场景图是Item场景的图形表示,它是一个独立的结构,其中包含足以渲染所有项目的信息。设置完成后,就可以独立于项目状态对其进行操作和渲染。在许多平台上,场景图形甚至会在GUI线程准备下一帧状态时在专用渲染线程上进行渲染。

场景图的结构

场景图由许多预定义的节点类型组成,每种类型都有专门的用途。尽管我们将其称为场景图,但更精确的定义是节点树。该树是根据QML场景中的QQuickItem类型构建的,然后在内部由渲染该场景的渲染器处理该场景。节点本身不包含任何活动的绘图代码或虚拟paint()函数。

即使节点树主要由现有的Qt Quick QML类型在内部构建,用户也可以添加具有自己内容的完整子树,包括表示3D模型的子树。

节点

对于用户而言,最重要的节点是QSGGeometryNode。它用于通过定义其几何形状和材质来定义自定义图形。使用QSGGeometry定义几何形状,并描述图形图元的形状或网格。它可以是直线,矩形,多边形,许多不连续的矩形或复杂的3D网格。该材质定义如何填充此形状的像素。

一个节点可以有任意数量的子节点,并且将渲染几何节点,以便它们以子顺序出现,并且父级位于其子级之后。

常见的节点有:

QSGClipNode

在场景图中实现裁剪功能的节点

QSGGeometryNode

用于场景图中的所有渲染内容的节点

QSGNode

场景图中所有节点的基类的节点

QSGOpacityNode

用于更改节点的不透明度的节点

QSGTransformNode

在场景图中实现变换的节点

QSGRenderNode

表示一组针对场景所使用的图形API的自定义渲染命令。

通过子类QQuickItem :: updatePaintNode()并设置QQuickItem :: ItemHasContents标志,将自定义节点添加到场景图。

至关重要的是,本机图形(OpenGL,Vulkan,Metal等)操作以及与场景图的交互必须专门在渲染线程上进行,主要是在updatePaintNode()调用期间进行。经验法则是仅在QQuickItem :: updatePaintNode()函数内使用带有“ QSG”前缀的类。

处理过程

节点具有虚拟QSGNode :: preprocess()函数,该函数将在呈现场景图之前被调用,主要用于处理节点要渲染的内容。节点子类可以设置标志QSGNode :: UsePreprocess并重写QSGNode :: preprocess()函数以对其节点进行最终准备。例如,将贝塞尔曲线划分为当前比例因子的正确细节级别或更新纹理的一部分。

节点的所有权

节点的所有权由创建者或场景图通过设置标志QSGNode :: OwnedByParent明确完成。通常,将所有权分配给场景图通常是可取的,因为这样可以简化场景图位于GUI线程之外时的清理操作。

材质

材质描述了如何填充QSGGeometryNode中几何图形的内部。它封装了用于图形管线顶点和片段阶段的图形着色器,并提供了足够的灵活性,尽管大多数Qt Quick项目本身仅使用非常基本的材质,例如纯色和纹理填充。

对于只想将自定义阴影应用于QML Item类型的用户,可以使用ShaderEffect类型在QML中直接执行此操作。

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

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包含一些便利类,以使最常见的自定义节点易于使用。

QSGSimpleRectNode-QSGGeometryNode子类,它使用纯色材质定义矩形几何。

QSGSimpleTextureNode-QSGGeometryNode子类,它使用纹理材质定义矩形几何形状。

场景图与渲染

场景图的呈现发生在QQuickWindow类的内部,并且没有公共API可以访问它。但是,呈现管道中有一些地方可供用户附加应用程序代码。可通过直接调用场景图使用的图形API(OpenGL,Vulkan,Metal等)来添加自定义场景图内容或插入任意渲染命令。这个集成点由渲染循环定义。

共有三种渲染循环变体:基本,窗口和线程。其中,基本和窗口是单线程的,而线程在专用线程上执行场景图渲染。 Qt尝试根据平台以及可能使用的图形驱动程序选择合适的循环。如果这不令人满意,或者出于测试目的,则可以使用环境变量QSG_RENDER_LOOP强制使用给定的循环。要验证使用哪个渲染循环,请启用qt.scenegraph.general日志记录类别。

线程和Windows渲染循环依赖于图形API实现来进行节流,例如,在OpenGL的情况下,通过请求交换间隔为1。一些图形驱动程序允许用户忽略此设置并将其关闭,而忽略Qt的请求。在不阻塞交换缓冲区操作(或其他位置)的情况下,渲染循环将以太快的速度运行动画并使CPU旋转100%。如果已知系统无法提供基于vsync的限制,请使用基本渲染循环,而不是在环境中设置QSG_RENDER_LOOP = basic。

基于线程的渲染循环

在许多配置中,场景图渲染将在专用渲染线程上进行。这样做是为了增加多核处理器的并行度,并更好地利用停顿时间,例如等待阻塞交换缓冲区调用。这可以显着提高性能,但是对与场景图进行交互的位置和时间施加了某些限制。

以下是有关如何使用线程渲染循环和OpenGL渲染帧的简单概述。除了OpenGL上下文的特定要求外,其他图形API的步骤也相同。

1、QML场景中发生更改,导致调用QQuickItem :: update()。例如,这可能是动画或用户输入的结果。事件被发布到渲染线程以启动新帧。

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

3、在渲染线程准备新帧时,GUI线程调用QQuickItem :: updatePolish()对项目进行最终修饰,然后再渲染它们。

4、阻塞GUI线程。

5、发出QQuickWindow :: beforeSynchronizing()信号。应用程序可以对此信号进行直接连接(使用Qt :: DirectConnection),以进行调用QQuickItem :: updatePaintNode()之前所需的任何准备工作。

6、将QML状态同步到场景图中。这是通过在自上一帧以来已更改的所有项目上调用QQuickItem :: updatePaintNode()函数来完成的。这是QML项与场景图中的节点唯一的交互。

7、释放GUI线程。

8、渲染场景图

8.1、发出QQuickWindow :: beforeRendering()信号。应用程序可以对此信号进行直接连接(使用Qt :: DirectConnection),以使用自定义图形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平台、具有Metal的macOS平台、移动平台、具有EGLFS的嵌入式Linux平台以及所有Vulkan平台上使用,但这可能会有所更改。通过在环境中设置QSG_RENDER_LOOP = threaded,始终可以强制使用线程渲染器。

有关frameSwapped信号

当帧已排队等待呈现时,将发出此信号。启用垂直同步后,在连续动画场景中,每个vsync间隔最多发射一次信号。该信号将从场景图形渲染线程中发出。

渲染线程的渲染代码:

代码语言:javascript复制
qtdeclarativesrcquickscenegraphqsgthreadedrenderloop.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)

当前,默认情况下,非线程渲染循环在具有ANGLE或非默认opengl32实现的Windows,具有OpenGL的macOS和具有某些驱动程序的Linux上使用。对于后者,这主要是一种预防措施,因为并非所有OpenGL驱动程序和窗口系统的组合都已经过测试。同时,诸如ANGLE或Mesa llvmpipe之类的实现根本无法在线程渲染中正常运行,因此,对于这些线程,必须不要使用线程渲染。

在macOS和OpenGL上,使用XCode 10(10.14 SDK)或更高版本进行构建时,不支持线程渲染循环,因为这会选择在macOS 10.14上使用基于图层的视图。您可以使用Xcode 9(10.13 SDK)进行构建,以选择不支持图层支持,在这种情况下,线程渲染循环可用并且默认情况下使用。 Metal没有这样的限制。

默认情况下,Windows用于具有ANGLE的Windows上的非线程渲染,而当需要非线程渲染时,basic用于所有其他平台。

即使在使用非线程渲染循环时,也应该像使用线程渲染器一样编写代码,否则将使代码不可移植。

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

使用QQuickRenderControl自定义渲染控制

使用QQuickRenderControl时,将驱动渲染循环的责任转移到应用程序中。在这种情况下,不使用内置的渲染循环。取而代之的是,应由应用程序在适当的时候调用抛光,同步和渲染步骤。可以实现类似于上述行为的线程行为或非线程行为。

混合场景图和本机图形API

场景图提供了两种方法来集成应用程序提供的图形命令:通过直接发出OpenGL,Vulkan,Metal等命令,以及在场景图中创建纹理化节点。

通过连接到QQuickWindow :: beforeRendering()和QQuickWindow :: afterRendering()信号,应用程序可以直接在场景图渲染到的同一上下文中进行OpenGL调用。使用Vulkan或Metal之类的API,应用程序可以通过QSGRendererInterface查询本机对象,例如场景图的命令缓冲区,并在认为合适的情况下向其记录命令。如信号名称所示,用户随后可以在Qt Quick场景下或上方渲染内容。以这种方式集成的好处是不需要额外的帧缓冲区或内存来执行渲染,并且消除了可能昂贵的纹理化步骤。缺点是Qt Quick决定何时调用信号,这是唯一允许OpenGL应用程序绘制的时间。

另一个方法(当前仅适用于OpenGL)是创建一个QQuickFramebufferObject,将其渲染到其中,然后将其作为纹理显示在场景图中。 “场景图-渲染FBO”示例显示了如何完成此操作。还可以组合多个渲染上下文和多个线程以创建要在场景图中显示的内容。场景图-线程示例中的渲染FBO显示了如何完成此操作。

即使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.scenegraph.general和qt.rhi。*或设置QSG_INFO = 1的情况下启动应用程序。然后,这将在初始化期间将一些基本信息打印到调试输出上。

除公共API外,场景图还具有适应层,该适应层打开实现以进行硬件特定的适应。这是一个未公开的内部和专用插件API,可让硬件适应小组充分利用其硬件。这包括:

自定义纹理:特别是QQuickWindow :: createTextureFromImage的实现以及Image和BorderImage类型使用的纹理的内部表示。

自定义渲染器:适配层使插件可以决定如何遍历和渲染场景图,从而有可能针对特定硬件优化渲染算法或使用可提高性能的扩展。

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

自定义动画驱动程序:允许动画系统连接到低级显示设备的垂直刷新中,以获得平滑的渲染。

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/234958.html原文链接:https://javaforall.cn

0 人点赞