# LearnSceneGraph **Repository Path**: jaredtao/LearnSceneGraph ## Basic Information - **Project Name**: LearnSceneGraph - **Description**: learn Qt SceneGraph - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 2 - **Created**: 2022-01-11 - **Last Updated**: 2024-08-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LearnSceneGraph Qt Quick Scene Graph 入门案例 # 简介 一步一步学习Scene Graph的绘制原理,最终目标是在QtQuick中集成一个视频渲染器。 # 徽章预览 | [Windows][win-link]| [Ubuntu][ubuntu-link]|[MacOS][macos-link]| |---------------|---------------|-----------------| | ![win-badge] | ![ubuntu-badge] | ![macos-badge] | [win-link]: https://github.com/JaredTao/LearnSceneGraph/actions?query=workflow%3AWindows "WindowsAction" [win-badge]: https://github.com/JaredTao/LearnSceneGraph/workflows/Windows/badge.svg "Windows" [ubuntu-link]: https://github.com/JaredTao/LearnSceneGraph/actions?query=workflow%3AUbuntu "UbuntuAction" [ubuntu-badge]: https://github.com/JaredTao/LearnSceneGraph/workflows/Ubuntu/badge.svg "Ubuntu" [macos-link]: https://github.com/JaredTao/LearnSceneGraph/actions?query=workflow%3AMacOS "MacOSAction" [macos-badge]: https://github.com/JaredTao/LearnSceneGraph/workflows/MacOS/badge.svg "MacOS" # 环境 Windows 10 Qt 6.2.2 msvc 2019 # 说明 ## 1-Triangle 画一个三角形。 ![1](doc/1.png) 关键点1: QQuickItem::updatePaintNode 函数的运行机制 ```c++ QSGNode* Triangle::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* data) { (void)data; auto node = static_cast(oldNode); if (!node) { //仅第一次进入此函数时,节点为空,新创建一个。此处不用关心delete,SceneGraph内部释放。 node = new QSGGeometryNode; //创建必要的子节点 ... } //按需更新数据 ... //返回节点 return node; } ``` 这个机制贯穿了整个QtQuick,所有自绘QQuickItem都是这样子。 关键点2: `节点QSGNode`和`材质QSGMaterial` SceneGraph将所有要处理的对象分为两大类:节点和材质,再由此两个大类往下细分。 节点的作用就是组织整个渲染树的结构,材质则是依附在节点上,用来表述如何渲染节点。 ## 节点 `QSGNode`是最基本的节点,可以任意嵌套父节点、子节点。 下图列出了Qt 内置的node: ![QSGNode](doc/QSGNode.png) 其中 `几何节点QSGGeometryNode` 是用的频率比较高的, Qt内置的`QSGGeometry`类,提供了`Point2D`、`TexturedPoint2D`、`ColoredPoint2D`三种数据定义, 专门给 `几何节点QSGGeometryNode` 使用, 基本涵盖了常规的二维场景。 ## 材质 `QSGMaterial`是基本的材质,通过着色器shader、纹理texture等来控制渲染。 下图列出了Qt 内置的material: ![QSGMaterial](doc/QSGMaterial.png) "1-Triangle"代码中,几何节点使用了Point2D数据,材质使用了`QSGFlatColorMaterial`, 是固定的颜色。 此处测试过,同样的代码,`QSGFlatColorMaterial`在Qt5.15.2的运行结果,是三角形只有边线有颜色, 在Qt6.2.2就有了填充色。 # 2-TriangleWithColor ![2](doc/2.png) 在"1-Triangle"的基础上,几何节点改用ColoredPoint2D,材质换成`QSGVertexColorMaterial`, 通过逐顶点设置颜色,就能看到五颜六色填充的三角形。 # 3-TriangleWithTransform ![3](doc/3.png) 在"2-TriangleWithColor"的基础上,几何节点改成子节点,给它套一个父节点`变换节点QSGTransformNode`, `变换节点QSGTransformNode`通过矩阵设置旋转,会带动子节点也跟着旋转。 于是就看到了彩色的三角形在旋转。 not: SceneGraph 应该是剔除了3D部分(深度),只能在旋转z轴时看到效果,x轴和y轴旋转会被丢掉,什么也看不见。 # 4-CustomGeometry 画曲线。 ![4](doc/4.png) 参考Qt Example - Custom Geometry。 在"1-Triangle"的基础上, 几何节点中的点数量扩充,前面是三角形就3个点、四边形就4个点, 此处画曲线,就需要更多的点,取了一个估值32 (你也可以改成其它的数量), 每个点的坐标,则根据曲线方程计算出来。 # 5-SimpleTexture 简易贴纹理 ![5](doc/5.png) Qt Scene Graph 中代表纹理的对象有`QSGTexture`和`QSGDynamicTexture`。 `QSGTexture`是一般的纹理 `QSGDynamicTexture`即动态纹理,用的不多,几乎找不到Qt官方的例子,可以忽略了。 ## 纹理的创建 创建方式是: 1、 读取一个QImage,然后通过`QQuickWindow::createTextureFromImage()`生成一个`QSGTexture`。 2、 通过`纹理提供者QSGTextureProvider`提供纹理。 方法一中,每一个QQuickItem都可以通过window()成员函数,取到QQuickWindow指针,然后调用接口即可。 方法二中,要说明一下纹理提供者。 纹理提供者可以将自身显示的内容作为一张纹理,给其它Item使用。(通过多遍渲染或者延迟渲染实现) 每一个QQuickItem都有`bool isTextureProvider() const`和`QSGTextureProvider *textureProvider() const` 两个函数, 前者判断item是否为纹理提供者,后者获取纹理提供者指针,进而可以获取到纹理。 大部分内置Item都不是纹理提供者, Qml中的Image、Canvas、ShaderEffectSource和开了layer的Item, 可以作为纹理提供者, 以及c++代码中继承于QQuickFrameBufferObect或者QQuickPaintedItem实现的 自定义item, 也是纹理提供者。 ## 纹理的使用 纹理的使用,可以直接在封装好的纹理节点中: 1、 简单纹理节点`QSGSimpleTextureNode` 也可以通过材质的方式使用: 2、 不透明材质`QSGOpaqueTextureMaterial` 3、 纹理材质`QSGTextureMaterial` 这里给出纹理和节点、材质的关系图 ![texture](doc/texture2.png) 4、 这几种用法不能满足需求的时候,也可以自定义材质来使用,后面专门讲。 此示例使用了简单纹理节点`QSGSimpleTextureNode` # 6-TextureTransform 纹理+坐标变换 ![6](doc/6.png) 前面示例的基础上, `QSGSimpleTextureNode` 套一个父节点`QSGTransformNode`。 # 7-Rectangle Rectangle ![7](doc/7.png) 通过`QSGRectangleNode`,就可以实现一个跟Qml中Rectangle 一模一样的 Item了。 # 8-Shader 自定义shader的使用 ![8](doc/8.png) 此示例通过`自定义节点` 和 `自定义材质`, 贴了一个纹理。 自定义节点,和前面的示例中代码大致相同。 自定义材质,主要是继承于材质接口类`QSGMaterial`,实现其接口。 ## QSGMaterial `QSGMaterial`主要接口定义如下 ```C++ virtual int compare(const QSGMaterial *other) const virtual QSGMaterialShader *createShader(QSGRendererInterface::RenderMode renderMode) const = 0 QSGMaterial::Flags flags() const void setFlag(QSGMaterial::Flags flags, bool on = true) virtual QSGMaterialType *type() const = 0 ``` 其中createShader接口要求返回一个`QSGMaterialShader`, `QSGMaterialShader`就代表着色器。 ## Qt6的 Shader 在Qt5中,SceneGraph的图形API默认是OpenGL(或OpenGL ES),Qt中的shader几乎都是默认用 OpenGL version 150 版本编写的(Qt为了最大化兼容,使用此版本,你可以手动指定高版本), 到Qt6的时候,Qt引入了Qt Shader Tools工具(简称qsb, Qt6的bin路径下, 可以找到命令行程序qsb.exe), 从此以后shader都会以二进制文件的方式加载。 ## Qt6 Shader新标准-兼容Vulkan的GLSL shader的语法标准,换成了`Vulkan-compatible GLSL`, 兼容Vulkan的一种GLSL, 详细的特性可以参考Khronos的文档[GL_KHR_vulkan_glsl](https://github.com/KhronosGroup/GLSL/blob/master/extensions/khr/GL_KHR_vulkan_glsl.txt) qml中的ShaderEffect,QSGMaterialShader,还有Qt3D都用这一套标准。 Qt6 对于shader有一些约定的用法,ShaderEffect的文档中有详细的说明。 (这些约定对于ShaderEffect,是必须遵循的,其它地方可以自行处理,QSGMaterialShader建议也遵循) 下面是此示例中用到的顶点着色器代码,此处也重点说明一下约定相关的内容: ```glsl #version 440 layout(location = 0) in vec4 qt_VertexPosition; layout(location = 1) in vec2 qt_VertexTexCoord; layout(location = 0) out vec2 qt_TexCoord; layout(std140, binding = 0) uniform buf { mat4 qt_Matrix; float qt_Opacity; } ubuf; void main() { gl_Position = ubuf.qt_Matrix * qt_VertexPosition; qt_TexCoord = qt_VertexTexCoord; } ``` 输入属性的0号位值,绑定顶点,1号位置绑定纹理坐标。 如果有多组纹理坐标,就按编号递增。 uniform必须放进一个block里面,std=140 不能变,binding 到0也不能变, qt_Matrix和qt_Opacity必须写在最前面,且这两个名字也不能变(SceneGraph内部用了这两个约定的名字,改了就对不上了)。 如果有额外的纹理,可以开新的uniform,比如这样: `glsl layout(binding = 1) uniform sampler2D tex; ` 下面是此示例中用到的片段着色器代码: ```glsl #version 440 layout(location = 0) in vec2 qt_TexCoord; layout(location = 0) out vec4 fragColor; layout(std140, binding = 0) uniform buf { mat4 qt_Matrix; float qt_Opacity; } ubuf; layout(binding = 1) uniform sampler2D qt_Texture; void main() { vec4 tColor = texture(qt_Texture, qt_TexCoord); fragColor = tColor; } ``` ## Qt6 Shader 的编译 Qt6中,只要编写一套shader,通过qsb编译一次,即可生成同时支持OpenGL、DirectX、Metal、Vulkan多种图形api的shader二进制文件。 此示例有如下cmd脚本,用来编译shader ```bat set QTDIR=D:\Qt\6.2.2\msvc2019_64 set PATH=%QTDIR%\bin;%PATH% qsb -b --glsl "150,120,100 es" --hlsl 50 --msl 12 -o cusTexture.vert.qsb cusTexture.vert qsb --glsl "150,120,100 es" --hlsl 50 --msl 12 -o cusTexture.frag.qsb cusTexture.frag ``` 顶点着色器文件名cusTexture.vert,编译结果cusTexture.vert.qsb 以qsb为扩展名,片段着色器同理。 在代码中,直接加载qsb结尾的文件即可。 # 9-Round 自定义shader的使用 ![9](doc/9.png) 此示例在"8-Shader"基础上,将矩形纹理处理成圆形,通过"半径"参数控制半径。 # 10-YUVRender ![10](doc/10.png) 此示例读取了一个YUV420格式的视频文件,并逐帧显示。 shader是一个常规的yuv转rgb算法。 SceneGraph中,每一帧图片分成三个QImage,分别通过`QQuickWindow::createTextureFromImage()`生成`QSGTexture`并更新到shader中。 这种方法性能很差(频繁创建、销毁纹理),仅用来教学,实际项目中不要用。 # 11-YUVRender-Dynamic ![11](doc/11.png) 此示例是对"10-YUVRender"的改进。 不再频繁创建、销毁纹理了,实现了纹理的复用、动态更新内容。 ## RHI纹理 之前的纹理都是通过`QQuickWindow::createTextureFromImage()`进行创建的,必然要用到QImage, 这个函数在Qt5的时候,SceneGraph底层默认的图形接口是OpenGL,其本质大致是通过 glGenTexture(...) glTextureImage2D(...) 这两个OpenGL的接口函数,实现纹理的创建和数据传输。 Qt5.14开始,增加了Rhi之后,可以同时兼容OpenGL、Vulkan、Metal、DirectX,不过部分SceneGraph功能还没改完。 从Qt6开始,则全部切换到了Rhi,不再直接使用图形接口了,而是通过Rhi实现。 Rhi中有了newTexture、uploadTexture等函数,就是封装了纹理的创建和数据更新,当然此处操作的对象是QSGRhiTexture。 ```c++ class QRhi { ... // Rhi纹理的创建 QRhiTexture *newTexture(QRhiTexture::Format format, const QSize &pixelSize, int sampleCount = 1, QRhiTexture::Flags flags = {}); ... }; ``` ```c++ class QRhiResourceUpdateBatch { ... //Rhi纹理的数据更新 void uploadTexture(QRhiTexture *tex, const QRhiTextureUploadDescription &desc); void uploadTexture(QRhiTexture *tex, const QImage &image); ... }; ``` 其中纹理数据的更新,需要填充几个结构体,大致如下: ```c++ if (!d->mData.isEmpty()) { QRhiTextureSubresourceUploadDescription subresDesc(d->mData.constData(), d->mData.size()); subresDesc.setSourceSize(d->mSize); subresDesc.setDestinationTopLeft(QPoint(0,0)); QRhiTextureUploadEntry entry(0,0, subresDesc); QRhiTextureUploadDescription desc(entry); resourceUpdates->uploadTexture(d->mRhiTexturePtr.get(), desc); d->mData.clear(); } ``` ## RHI纹理在SceneGraph中的使用 首先要说明的是,从Qt5到Qt6,SceneGraph的整体框架流程及接口并没有改变,只是实现方式替换成了Rhi。 举例来说,纹理还是使用的QSGTexture,QSGTexture内部实现使用了QRhiTexture。 "11-YUVRender-Dynamic"示例中,就封装了一个YUVTexture类,本身继承于`QSGTexture`,内部的纹理操作则使用QRhiTexture "11-YUVRender-Dynamic"的运作流程大致如下: * QQuickItem数据更新 * Scene Graph RenderLoop 更新节点树 * QQuickItem::updatePaintNode 更新QSGNode,QSGNode标记对应的DirtyMaterial * Scene Graph RenderLoop 处理Dirty Material, 调用QSGMaterialShader::updateUniformData和QSGMaterialShader::updateSampledImage * QSGMaterialShader::updateUniformData 中调用了Material的QSGTexture::updateTexture * QSGTexture::updateTexture内部调用QRhiTexture的commitTextureOperations * QRhiTexture::commitTextureOperations即前面提到的纹理数据更新 ```c++ #pragma once #include #include struct YUVTexturePrivate; class YUVTexture : public QSGTexture { public: YUVTexture(); ~YUVTexture(); // QSGTexture interface public: qint64 comparisonKey() const override; QRhiTexture *rhiTexture() const override; QSize textureSize() const override; bool hasAlphaChannel() const override; bool hasMipmaps() const override; void commitTextureOperations(QRhi *rhi, QRhiResourceUpdateBatch *resourceUpdates) override; public: void setRhiTexture(QRhiTexture *rhiTexture); void setData(QRhiTexture::Format f, const QSize &s, const QByteArray &data); private: YUVTexturePrivate *d = nullptr; }; ``` # 赞助 觉得分享的内容还不错,就请作者喝杯咖啡吧