Qt5 QtQuick系列----QtQuick的Secne Graph剖析(1)
教是言词, 实不是道,道本无言, 言说是妄。------- 达摩
Qt 5提出了一个新的渲染底层,以替代Qt4时期的Graphics View,这个渲染底层就是Scene Graph。Scene Graph主要利用OpenGL ( ES )2的渲染优势,在2D和3D以非常流畅的速度进行渲染,满足日益增长的界面效果需求,同时Scene Graph预留了各种各样的接口,满足大家定义显示和渲染效果的需要。
该文章下面部分,主要来自Qt官方的说明文档:http://doc.qt.io/qt-5/qtquick-visualcanvas-scenegraph.html 并结合了自己的理解。
在 Qt Quick 2 中使用了基于OpenGL ES 2.0或OpenGL 2.0的场景图来进行渲染。使用场景图进行绘图而不是传统的绘图机制(QPainter类似的)可以在下一帧被渲染之前,提前计算出将要渲染帧的全部信息。这就为进行优化创造了条件,例如可以进行的优化有:通过分批渲染来减小状态的切换;渲染时直接丢弃被覆盖的元素。
例如,假设一个用户界面包含一个10个item,每个item包括一个背景颜色,一个图标,一个文本。使用传统的绘图方法,需要30次的绘制调用(比如调用30次opengl),导致很多的状态切换(例如opengl是基于状态机的,每次状态的切花都会有一定的开销)。 如果用场景图的方法的话,只需要3次绘制,一次绘制所有的背景,然后是所有的图标,然后是所有的文本,从而大大减少调用次数,大大提高性能。
Qt5的场景图与Qt Quick 2.0紧密相连,不能单独使用。 QQuickWindow 类负责对场景图进行管理并将其渲染出来(负责将场景图交给opengl)。通过调用QQuickItem::updatePaintNode()函数,用户可以将自定义的图元添加到场景图中。
假设你在qml文件中定义了一系列的item,这些你定义的item最终被通过场景图表征出来,场景图中包含了足够的信息,利用这些信息可以将你定义的界面表示出来。 一旦为qml文件中定义的items建立了场景图,这个场景图便可以独立于这些item存在了,然后,可以有一个专门的线程来基于场景图的信息将界面渲染到显示器上(有些平台上没有单独的渲染线程),在这个渲染线程进行渲染的同时,GUI线程可以准备下一帧的数据了。
注意: Much of the information listed on this page is specific to the default OpenGL adaptation of the Qt Quick Scene graph. For more information about the different scene graph adaptations see Scene Graph Adaptations.
下面先来看一下场景图的结构吧。
Qt Quick Scene Graph Structure
QtQuick场景图其实是一个树结构,包含很多个节点。这个树是从何而来呢?官方说法:
The tree is built from QQuickItem types in the QML scene and internally the scene is then processed by a renderer which draws the scene.(QquickItem是最基本的qml C++类型)
即: QtQuick场景图源自于你在qml文件中写的那些基本的qml类型,例如Rectangle,如果从qml引擎开始分析的话,大体过程我想应该是这样的:首先,qml引擎加载qml文件,它会为你定义的每个基本类型创建对应的C++类对象,例如,为Rectangle创建QQuickRectangle类对象 (https://blog.csdn.net/qq_35865125/article/details/85869276 ),这些被创建的对象之间应该是有层次关系的,最终,这些对象被转换成场景图,场景图的每个节点对应的C++类是QSGNode。
Qml引擎创建的c++对象们(例如QQuickRectangle类对象)是如何被转换成场景图中的一个个QSGNode类对象的? -- 是通过调用QSGNode* QQuickItem::updatePaintNode()来完成的!(QquickItem类是QQuickRectangle等类的基类) QquickItem类通过该函数将自己转换成QSGNode类对象,应该是将自己的位置属性,颜色属性等等信息都转到其返回的QSGNode*里面吧,具体参考官方文档对QquickItem的说明:http://doc.qt.io/qt-5/qquickitem.html#updatePaintNode 。至于何时调用updatePaintNode,本文后面会介绍。
认识下QquickItem:
The QQuickItem class provides the most basic of all visual items in Qt Quick.
All visual items in Qt Quick inherit from QQuickItem. Although a QQuickItem instance has no visual appearance, it defines all the attributes that are common across visual items, such as x and y position, width and height, anchoring and key handling support.
You can subclass QQuickItem to provide your own custom visual item that inherits these features.
它应该是QQuickRectangle类的基类,注意:QQuickRectangle这种基本的qml类型对应的类是用户不可见的。
场景图被建立起来之后,后面会被送给渲染线程将其渲染到显示器上。
文档上说:“The nodes themselves do not contain any active drawing code nor virtual paint()
function”,即:场景图中的节点对应的类并不包含绘制函数,所以才需要渲染线程来将场景图画出来吧。
另外,虽然场景图一般是根据基本的qml类型建立起来的,用户是可以向场景图中添加自定义类型的,也可以添加3D模型。我使用过的添加自定义类型的方式:定义自己的类,继承自QQuickItem,然后,向qml中注册这个类,并在qml文件中使用这个类,这在介绍QQuickItem类的文档中有提到。是否可以在场景图生成之后,向其中添加自己生成的QSGNode节点?
qt场景图的Nodes:
对用户来说最重要的节点是QSGGeometryNode。可以使用该类定义用户图形,例如 可以定义几何形状,材料。几何形状可以用QSGGeometry类来定义,其可以描述形状或曲面(mesh),形状可以是直线,矩形,多边形,或 许许多多的不相连的小三角形,或 复杂的三维曲面。
一个节点可以有任何数量的子节点:“A node can have any number of children and geometry nodes will be rendered so they appear in child-order with parents behind their children.”
注意: This does not say anything about the actual rendering order in the renderer. Only the visual output is guaranteed。
用户可用的节点类型:
注意:“Custom nodes are added to the scene graph by subclassing QQuickItem::updatePaintNode() and setting the QQuickItem::ItemHasContents flag.”---- 这句话在强调如果你自己定义了继承自QQuickItem类型的子类,别忘了设置QQuickItem::ItemHasContents标识,设置了标识之后updatePainNode函数才会被触发,具体见官方文档对updatePainNode函数的说明。(qml引擎首先生成一个对象树,该树的节点都是基本的qml类型或者你自己定义的QQuickItem子类, 然后在渲染线程中,opengl要渲染之前,需要触发QQuickItem::UpdatPainNode函数得到场景图树结构)。
警告: “It is crucial that OpenGL operations and interaction with the scene graph happens exclusively on the render thread, primarily during the updatePaintNode() call. The rule of thumb is to only use classes with the "QSG" prefix inside the QQuickItem::updatePaintNode() function.” --- OpenGL与qt场景图之间的交互只能发生在渲染线程中,两者的交互是通过QQuickItem::updatePaintNode()函数来实现的,一个重要的原则是只在该函数中使用QSG为前缀的类。
看来,所谓的用户自己向qt场景图中添加自定义节点的方式是这样的:首先,需要自定义一个继承自QquickItem的类,在这个类中重写QQuickItem::updatePaintNode()函数,在该函数中生成QSGNode类型的节点并返回,,那些不能被用户看到的QquickRectangle类应该也是如此吧。
更多信息,请移步 Scene Graph - Custom Geometry.
场景图Node的Preprocessing
Nodes have a virtual QSGNode::preprocess() function, which will be called before the scene graph is rendered. Node subclasses can set the flag QSGNode::UsePreprocess and override the QSGNode::preprocess() function to do final preparation of their node. For example, dividing a bezier curve(贝塞尔曲线) into the correct level of detail for the current scale factor or updating a section of a texture. ---可以重载函数QSGNode::preprocess()做一些处理,若要此函数在场景被渲染之前被执行一下子,需要设置一个标识。
场景图Node的所有权(Node Ownership)
Ownership of the nodes is either done explicitly by the creator or by the scene graph by setting the flag QSGNode::OwnedByParent. Assigning ownership to the scene graph is often preferable as it simplifies cleanup when the scene graph lives outside the GUI thread.
---设置了这个标识后,该节点的父节点被自动设置吧,根据的是你在qml文件中定义的层级结构吧。
Materials
The material describes how the interior of a geometry in a QSGGeometryNode is filled. It encapsulates an OpenGL shader program and provides ample flexibility in what can be achieved, though most of the Qt Quick items themselves only use very basic materials, such as solid color and texture fills.
material描述了QSGGeometryNode的几何图形的内部该如何被填充。它封装了一个OpenGL的着色程序并且很灵活。尽管大部分的Qt Quick item自己只使用一些很基础的material,比如使用纯色填充或是文本填充。
对于那些只是想在QML基本类型中使用自定义描影的的用户,可以直接在QML里面使用ShaderEffect类型。
更多内容请看: Scene Graph - Simple Material
Convenience Nodes
The scene graph API is very low-level and focuses on performance rather than convenience. Writing custom geometries and materials from scratch, even the most basic ones, requires a non-trivial amount of code. For this reason, the API includes a few convenience classes to make the most common custom nodes readily available.
- QSGSimpleRectNode - a QSGGeometryNode subclass which defines a rectangular geometry with a solid color material.
- QSGSimpleTextureNode - a QSGGeometryNode subclass which defines a rectangular geometry with a texture material.
Scene Graph and Rendering
场景图的渲染过程发生在QQuickWindow类的内部,对于这个渲染过程,qt没有提供public类型的API。但是,QquickWindow类在渲染过程中,允许执行用户自定以的代码,例如,渲染进行到某个接断时,会发出一个信号,用户接收到信号之后,可以去执行一段代码,这段代码可以是”add custom scene graph content” 或者 “render raw OpenGL content”。
For detailed description of how the scene graph renderer for OpenGL works, see Qt Quick Scene Graph OpenGL Renderer.该文档主要将opengl的知识,有一定基础的人可以看。
对于渲染,有三个变量: basic, windows, 和 threaded。其中,basic和windows对应单线程, threaded对应的是在一个专门的线程中执行场景图的渲染。针对不同的平台和使用的显卡驱动,Qt试图选择一个合适的变量。可以使用QSG_RENDER_LOOP环境变量来强制使用一个指定变量。想要确定哪种方式正在被使用,可以“enable the qt.scenegraph.general
logging category”。
注意: 有些平台上,不能设置swap interval的值,如果swap interval的值为0,会导致渲染线程过快地运行动画,即画面更新太快,导致100%地占用cpu, 在这种平台上需要在环境中设置QSG_RENDER_LOOP=basic,来
使用basic变量。 关于opegl的swap interval: https://www.khronos.org/opengl/wiki/Swap_Interval :
The term "swap interval" itself refers to the number of v-blanks that must occur before the front and back frame buffers are swapped. A swap interval of 1 tells the GPU to wait for one v-blank before swapping the front and back buffers. A swap interval of 0 specifies that the GPU should never wait for v-blanks, thus performing buffer swaps as soon as possible when rendering for a frame is finished. Video drivers can override these values, forcing a swap interval of 1 or 0 depending on settings the user provided in the video card's control panel.
Threaded Render Loop ("threaded")—单独一个渲染线程
很多情况下,场景图的渲染发生在一个单独的线程里面,这可以增加多核系统的并行度,例如在渲染线程在swap interval时,影响不到其他线程的执行。这大大提高了性能,但是对gui线程和渲染线程之间的交互增加了一些限制。
The following is a simple outline of how a frame gets composed with the threaded render loop:
- QML画面场景发生变化后(例如,动画,用户输入了东西),导致QQuickItem::update()被调用,然后,gui线程会发送一个信号线程给渲染线程,要求它新搞一个frame。 ---(如果qml画面场景中有很多个item,只有一个item发生变化后,应该不会导致所有的item对应的QQuickItem::update()被调用)。
- The render thread prepares to draw a new frame and makes the OpenGL context current and initiates a block on the GUI thread:: 渲染线程开始准备画一个新的frame, 设置opengl环境,并请求让gui线程阻塞。
- While the render thread is preparing the new frame, the GUI thread calls QQuickItem::updatePolish() to do final touch-up of items before they are rendered.::当渲染线程准备新的frame时,gui线程调用QQuickItem::updatePolish()来对各个item做最后的润色。
- GUI thread is blocked. ::gui线程被阻塞
- The QQuickWindow::beforeSynchronizing() signal is emitted. Applications can make direct connections (using Qt::DirectConnection) to this signal to do any preparation required before calls to QQuickItem::updatePaintNode(). ::渲染线程发出QQuickWindow::beforeSynchronizing()信号,用户可以在其他线程接受这个信号,在QQuickItem::updatePaintNode()被调用之前做一些事情。注意:connect该信号的方式必须是Qt::DirectConnection,以保证槽函数在渲染线程中被执行,以保证顺序。
- Synchronization of the QML state into the scene graph. This is done by calling the QQuickItem::updatePaintNode() function on all items that have changed since the previous frame. This is the only time the QML items and the nodes in the scene graph interact.::根据qml场景生成qt场景图,这是通过调用发生状态变化的qml item调用其QQuickItem::updatePaintNode()来实现的。这是唯一的qml items与qt场景图进行交互的地方。
- GUI thread block is released.::gui线程的阻塞停止,继续运行。
- The scene graph is rendered:
- The QQuickWindow::beforeRendering() signal is emitted. Applications can make direct connections (using Qt::DirectConnection) to this signal to use custom OpenGL calls which will then stack visually beneath the QML scene. 场景图开始被渲染之前,QquickWindow类会发出beforeRendering信号,用户可以接受信号,让渲染线程执行一段代码,例如可以调用OpenGl函数进行绘图,这时绘制的opengl图将会被后面生成的场景图对应的画面覆盖哦。
- Items that have specified QSGNode::UsePreprocess, will have their QSGNode::preprocess() function invoked. ::对一些场景图中的节点,触发QSGNode::preprocess()函数。
- The renderer processes the nodes and calls OpenGL functions.::渲染线程根据场景图中节点信息调用opengl函数,将画面搞出来!
- The QQuickWindow::afterRendering() signal is emitted. Applications can make direct connections (using Qt::DirectConnection) to this signal to use custom OpenGL calls which will then stack visually over the QML scene. ::场景图始被渲染完后,QquickWindow类会发出afterRendering信号,用户可以接受信号,让渲染线程执行一段代码,例如可以调用OpenGl函数进行绘图,这时绘制的opengl图将会覆盖后面生成的场景图对应的画面哦。
- The rendered frame is swapped and QQuickWindow::frameSwapped() is emitted.::渲染线程swap frame,发出frameSwapped信号。
- The QQuickWindow::beforeRendering() signal is emitted. Applications can make direct connections (using Qt::DirectConnection) to this signal to use custom OpenGL calls which will then stack visually beneath the QML scene. 场景图开始被渲染之前,QquickWindow类会发出beforeRendering信号,用户可以接受信号,让渲染线程执行一段代码,例如可以调用OpenGl函数进行绘图,这时绘制的opengl图将会被后面生成的场景图对应的画面覆盖哦。
- While the render thread is rendering, the GUI is free to advance animations, process events, etc.::gui线程和渲染线程继续高歌猛进。
在以下情况下,对于渲染,默认使用threaded变量:Windows with opengl32.dll, Linux with non-Mesa based drivers, macOS, mobile platforms, and Embedded Linux with EGLFS。也可以在环境中设置QSG_RENDER_LOOP=threaded来强制使用该变量。
Non-threaded Render Loops ("basic" and "windows") – gui线程内渲染
理解了上面的内容,对于gui线程内渲染的情况就容易理解了:
The non-threaded render loop is currently used by default on Windows with ANGLE or a non-default opengl32 implementation and Linux with Mesa drivers. For the latter this is mostly a precautionary measure, as not all combinations of OpenGL drivers and windowing systems have been tested. At the same time implementations like ANGLE or Mesa llvmpipe are not able to function properly with threaded rendering at all so not using threaded rendering is essential for these.
By default windows is used for non-threaded rendering on Windows with ANGLE, while basic is used for all other platforms when non-threaded rendering is needed.
Even when using the non-threaded render loop, you should write your code as if you are using the threaded renderer, as failing to do so will make the code non-portable.
The following is a simplified illustration of the frame rendering sequence in the non-threaded renderer.
Custom control over rendering with QquickRenderControl—使用QquickRenderControl来自己控制渲染过程
When using QQuickRenderControl, the responsibility for driving the rendering loop is transferred to the application. In this case no built-in render loop is used. Instead, it is up to the application to invoke the polish, synchronize and rendering steps at the appropriate time. It is possible to implement either a threaded or non-threaded behavior similar to the ones shown above.:: 使用该类时,驱动渲染线程的任务由qt用户自己完成。
Ref:
https://www.tuicool.com/articles/7jaYzq
https://blog.csdn.net/gamesdev/article/details/43063219
https://blog.csdn.net/gamesdev/article/details/43067265
https://blog.csdn.net/lainegates/article/details/50890551