Qt Quick Scene Graph 相关文档:https://doc.qt.io/qt-5/qtquick-visualcanvas-scenegraph.html
参照示例 customgeometry:https://doc.qt.io/qt-5/qtquick-scenegraph-customgeometry-example.html
本文完整代码链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qml/LearnQSG_20210614_Line
使用 C++ 自定义 QML 可视组件有多种方式:自定义 render,使用 QSG 开头的场景图类,使用 QPainter 配合 QQuickPaintedItem 绘制(就像 QWidget paintEvent 那样)等等。
大部分情况我们都会使用 QPainter + QQuickPaintedItem 的方式,QPainter 接口相对成熟,而且不用了解 GLSL 这些知识(QQuickPaintedItem 就是先用 QPainter 绘制到纹理上,然后再作为一个 QSG 节点渲染这个纹理)。对于一些有特殊需求的可能会自定义 render ,然后使用 OpenGL 或者其他图形接口进行操作。而 QSG 这一套场景图类使用的人很少,个人认为主要是基础工具类不全,很多 QPainter 的功能都不具备,比如抗锯齿和文字绘制目前就没公开的类来支持。我写这个纯属无聊学习一下。
QSG 的封装思路和大部分 3D 库/引擎的思路差不多。像一个可视节点 QSGGeometryNode 需要包含 geometry 顶点和 material 材质两部分, 类似于其他架构中的 entity、mesh、material。不过这套接口网上的资料较少,大部分时候还是得借鉴 Qt 示例和源码, 此外也有人基于 QSG 实现了一套自己的 Qt Quick 组件:https://github.com/uwerat/qskinny
参照示例 customgeometry,我写一个简单的例子,只填充了两个顶点,连成一条斜线。 首先,继承 QQuickItem, 实现 updatePaintNode 接口(类似于 QWidget 的 paintEvent 接口),接口中我们需要构造一个 QSGNode(可视节点使用 QSGGeometryNode ),然后将顶点坐标和颜色添加到该节点,最终会根据我们的顶点数据和着色器来进行渲染。有两点要注意,可视 Item 需要设置 setFlag(ItemHasContents, true);节点的某部分需要更新则需要设置 dirty 标志,如顶点更新了需要设置 node->markDirty(QSGNode::DirtyGeometry)。
#pragma once
#include <QQuickItem>
class QSGLine : public QQuickItem
{
Q_OBJECT
public:
explicit QSGLine(QQuickItem *parent = nullptr);
//渲染时会调用组件的此接口
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override;
};
#include "QSGLine.h"
#include <QtQuick/QSGNode>
#include <QtQuick/QSGFlatColorMaterial>
QSGLine::QSGLine(QQuickItem *parent)
: QQuickItem(parent)
{
//对于要渲染的组件,要设置此标志
setFlag(ItemHasContents, true);
}
QSGNode *QSGLine::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData)
{
Q_UNUSED(updatePaintNodeData)
//一个node包含geometry和material,类似于别的架构中的entity、mesh、material
//node可以是要渲染的物体,也可以是透明度等
//geometry定义了网格、顶点、结构等,比如坐标点
//material定义了填充方式
QSGGeometryNode *node = nullptr;
QSGGeometry *geometry = nullptr;
//第一次调用时为nullptr,后面每次会把原来的节点指针传进来
if (!oldNode) {
//QSGGeometryNode是一个便捷类,用于可渲染节点
node = new QSGGeometryNode;
//构造顶点,提供了三种简便设置
//defaultAttributes_Point2D(); 普通坐标点
//defaultAttributes_TexturedPoint2D(); 带纹理坐标点
//defaultAttributes_ColoredPoint2D(); 带颜色坐标点
//这里设置为了两个坐标点的结构,后面会设置坐标值
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2);
//线宽
geometry->setLineWidth(1);
//绘制模式,setDrawingMode(DrawLines)相当于OpenGL的glDrawArrays(GL_LINES)
geometry->setDrawingMode(QSGGeometry::DrawLines);
//添加到node
node->setGeometry(geometry);
//此设置表示该节点拥有Geometry实例的所有权,并且会在该节点被销毁或重新设置时将其释放
node->setFlag(QSGNode::OwnsGeometry);
//构造材质,这里使用的纯色
QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
//颜色
material->setColor(QColor(255, 0, 0));
//添加到node
node->setMaterial(material);
//此设置表示该节点拥有Material实例的所有权,并且会在该节点被销毁或重新设置时将其释放
node->setFlag(QSGNode::OwnsMaterial);
} else {
//初始化完成后,后面的刷新会进入到这个逻辑里
//这里我们可以更新geometry
node = static_cast<QSGGeometryNode *>(oldNode);
geometry = node->geometry();
//可以重置坐标点个数,比如刷新了数据,调用update,就可以在此处重新设置
//geometry->allocate(2);
}
//这个函数是同geometry构造参数对应的,相当于cast了一个内存块的指针
//如果是自定义的geometry结构,直接geometry->vertexData()拿指针就行
QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
vertices[0].set(0,0); //点1左上角
vertices[1].set(width(),height()); //点2右下角
//设置dirty标志后场景图才会刷新对应内容
//如果不设置dirty geometry,那么数据变化就不会刷新
//(在此例里表现为拖动大小后没有刷新ui渲染)
node->markDirty(QSGNode::DirtyGeometry);
return node;
}
上面使用的 Qt 提供的 geometry 和 material 类,我们也可以继承后自己扩展,下面是第二个例子。(由于代码较多,我把 Item 部分去掉了,只贴了 geometry 和 material 部分,
完整代码见:https://github.com/gongjianbo/MyTestCode/tree/master/Qml/LearnQSG_20210614_Line)
#pragma once
#include <QSGGeometry>
//参照源码:
//qt-everywhere-src\qtdeclarative\src\quick\scenegraph\coreapi\qsggeometry.h
//这里仿写一个ColoredPoint2D
class MyGeometry : public QSGGeometry
{
public:
using QSGGeometry::QSGGeometry;
//定义数据结构只是为了操作方便
struct My2D {
float x, y;
unsigned char r, g, b, a;
void set(float nx, float ny, uchar nr, uchar ng, uchar nb, uchar na = 255){
x = nx; y = ny; r = nr; g = ng; b = nb; a = na;
}
};
static const AttributeSet &defaultAttributes_My2D();
My2D *vertexDataAsMy2D();
const My2D *vertexDataAsMy2D() const;
};
#include "MyGeometry.h"
//QSGGeometry的构造函数需要AttributeSet参数
//struct AttributeSet {
// int count;
// int stride;
// const Attribute *attributes;
//};
const QSGGeometry::AttributeSet &MyGeometry::defaultAttributes_My2D()
{
static Attribute data[] = {
//对应material的坐标vertex
Attribute::createWithAttributeType(0, 2, FloatType, PositionAttribute),
//对应material的颜色vertex
Attribute::createWithAttributeType(1, 4, UnsignedByteType, ColorAttribute)
};
//count为属性个数,stride为vertex大小/步进
//vertexByteSize = m_attributes.stride * m_vertex_count;
static AttributeSet attrs = { 2, 2 * sizeof(float) + 4 * sizeof(char), data };
return attrs;
}
MyGeometry::My2D *MyGeometry::vertexDataAsMy2D()
{
return static_cast<My2D *>(vertexData());
}
const MyGeometry::My2D *MyGeometry::vertexDataAsMy2D() const
{
return static_cast<const My2D *>(vertexData());
}
#pragma once
#include <QSGMaterial>
#include <QColor>
//Qt部分demo使用了QSGSimpleMaterial,但是此类已标记为obsolete
//所以这里参照源码QSGFlatColorMaterial,以及vertexcolormaterial和texturematerial
//qt-everywhere-src\qtdeclarative\src\quick\scenegraph\util\qsgflatcolormaterial.h
class MyMaterial : public QSGMaterial
{
public:
MyMaterial();
//该函数由场景图调用以返回每个子类的唯一实例
QSGMaterialType *type() const override;
//对于场景图中存在的每种材质类型,createShader只会被调用一次,并将在内部缓存
QSGMaterialShader *createShader() const override;
//将此材料与其他材料进行比较,如果相等则返回0
//此material先排序为-1,其他material先排序为1.
//场景图可以重新排列几何节点以最小化状态变化。
//在排序过程中调用 compare 函数,以便可以对材质进行排序,
//以尽量减少每次调用QSGMaterialShader::updateState () 时的状态变化。
int compare(const QSGMaterial *other) const override;
//自定义接口用于设置材质属性
const QColor &getColor() const { return fragColor; }
void setColor(const QColor &color);
private:
QColor fragColor = QColor(0,0,0);
};
#include "MyMaterial.h"
#include <QSGMaterialShader>
//#include <QOpenGLContext>
//#include <QOpenGLFunctions>
class MyMaterialShader : public QSGMaterialShader
{
public:
MyMaterialShader()
{
//懒得加两个文件,用vertexShader返回
//setShaderSourceFile(QOpenGLShader::Vertex, QStringLiteral(":/qt-project.org/scenegraph/shaders/vertexcolor.vert"));
//setShaderSourceFile(QOpenGLShader::Fragment, QStringLiteral(":/qt-project.org/scenegraph/shaders/vertexcolor.frag"));
}
const char *vertexShader() const override
{
return R"(attribute highp vec4 vertexCoord;
attribute highp vec4 vertexColor;
uniform highp mat4 matrix;
uniform highp float opacity;
varying lowp vec4 color;
void main()
{
gl_Position = matrix * vertexCoord;
color = vec4(vertexColor.rgb*vertexColor.a,vertexColor.a) * opacity;
}
)";
}
const char *fragmentShader() const override
{
return R"(varying lowp vec4 color;
void main()
{
gl_FragColor = color;
})";
}
//刷新的时候会调用此函数,通过判断状态来决定哪些需要更新
void updateState(const RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override
{
Q_UNUSED(newEffect)
Q_UNUSED(oldEffect)
//QSG混合模式为glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA),提供了预乘混合
//FlatColorMaterialShader::updateState中预乘了透明度,但现在我把颜色作为顶点属性
//所glsl改为:color = vec4(vertexColor.rgb*vertexColor.a,vertexColor.a) * opacity;
if (state.isOpacityDirty()){
//颜色的变动也设置opacitydirty
program()->setUniformValue(opacityId, state.opacity());
//启用LINE_SMOOTH或者MSAA都会导致横向/竖向直线变胖
//if(state.context()&&state.context()->functions()){
// //state.context()->functions()->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// state.context()->functions()->glEnable(GL_BLEND);
// state.context()->functions()->glEnable(GL_LINE_SMOOTH);
// state.context()->functions()->glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
//}
}
if (state.isMatrixDirty())
program()->setUniformValue(matrixId, state.combinedMatrix());
}
//顶点属性名,数组最后一个填nullptr
//对应geometry的vertexData数据结构
char const *const *attributeNames() const override
{
static const char *const attr[] = { "vertexCoord", "vertexColor", nullptr };
return attr;
}
private:
void initialize() override
{
opacityId = program()->uniformLocation("opacity");
matrixId = program()->uniformLocation("matrix");
}
private:
int opacityId;
int matrixId;
};
MyMaterial::MyMaterial()
: QSGMaterial()
{
setFlag(Blending, true);
}
QSGMaterialType *MyMaterial::type() const
{
static QSGMaterialType type;
return &type;
}
QSGMaterialShader *MyMaterial::createShader() const
{
return new MyMaterialShader;
}
int MyMaterial::compare(const QSGMaterial *other) const
{
const MyMaterial *mate = static_cast<const MyMaterial *>(other);
return fragColor.rgba() - mate->getColor().rgba();
}
void MyMaterial::setColor(const QColor &color)
{
fragColor = color;
//带透明则启用混合
setFlag(Blending, fragColor.alpha() != 0xff);
}