Qt Quick Scene Graph 学习1:画线

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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值