Qt Quick Scene Graph learning 1: draw lines

Posted by tibiz on Sat, 29 Jan 2022 13:56:29 +0100

Qt Quick Scene Graph related documents: https://doc.qt.io/qt-5/qtquick-visualcanvas-scenegraph.html

Refer to the example customgeometry: https://doc.qt.io/qt-5/qtquick-scenegraph-customgeometry-example.html

Complete code link of this article: https://github.com/gongjianbo/MyTestCode2021/tree/master/Qml/LearnQSG_20210614_Line

There are many ways to use C + + to customize QML visual components: Customize render, use the scene diagram class starting with QSG, use QPainter to draw with QQuickPaintedItem (just like QWidget paintEvent), and so on.

In most cases, we will use the method of "QPainter + QQuickPaintedItem". The QPainter interface is relatively mature, and we don't need to know GLSL (QQuickPaintedItem is to draw the texture with "QPainter" first, and then render the texture as a QSG node). For some with special needs, you may customize render, and then use OpenGL or other graphical interfaces to operate. However, few people use the QSG scene map class. Personally, I think it is mainly due to the incomplete basic tool class. Many QPainter functions are not available, such as anti aliasing and text rendering. At present, there are no public classes to support it. I write this is just boring to learn.

The packaging idea of QSG library is similar to that of most 3D libraries / engines. Like a visual node, qsggeometriynode needs to include two parts: geometry vertex and material material, which are similar to entity, mesh and material in other architectures. Some people also customize QSG to implement their own set of Qt Quick components: https://github.com/uwerat/qskinny

The following is a simple example, referring to the example customgeometry, but only two vertices are filled to form a slash. First, inherit QQuickItem and implement the updatePaintNode interface (similar to the paintEvent interface of QWidget). In the interface, we need to construct a QSGNode (the visual node uses qsggeometriynode), and then add our vertex coordinates and colors to the node. There are two points to note: you need to set the "setflag" (itemhascontents, true) for visual items; Some part of the node needs to be updated when refreshing, and the dirty flag needs to be set. For example, when the vertex is updated, the setting node - > markdirty (QSGNode:: dirtygeometry) needs to be set.

#pragma once
#include <QQuickItem>

class QSGLine : public QQuickItem
{
    Q_OBJECT
public:
    explicit QSGLine(QQuickItem *parent = nullptr);
    //This interface of the component is called when rendering
    QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override;
};
#include "QSGLine.h"
#include <QtQuick/QSGNode>
#include <QtQuick/QSGFlatColorMaterial>

QSGLine::QSGLine(QQuickItem *parent)
    : QQuickItem(parent)
{
    //Set this flag for the component to be rendered
    setFlag(ItemHasContents, true);
}

QSGNode *QSGLine::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData)
{
    Q_UNUSED(updatePaintNodeData)
    //A node contains geometry and material, similar to entity, mesh and material in other architectures
    //node can be the object to be rendered, or it can be transparency, etc
    //geometry defines meshes, vertices, structures, etc., such as coordinate points
    //material defines the filling method
    QSGGeometryNode *node = nullptr;
    QSGGeometry *geometry = nullptr;

    //The first call is nullptr, and the original node pointer will be passed in each time
    if (!oldNode) {
        //Qsggeometriynode is a convenient class for renderable nodes
        node = new QSGGeometryNode;

        //Constructing vertices provides three simple settings
        //defaultAttributes_Point2D();  Common coordinate point
        //defaultAttributes_TexturedPoint2D();  Textured coordinate point
        //defaultAttributes_ColoredPoint2D();  Colored coordinate point
        //It is set here for the structure of two coordinate points, and the coordinate values will be set later
        geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2);
        //line width
        geometry->setLineWidth(1);
        //Drawing mode: setDrawingMode(DrawLines) is equivalent to OpenGL's glDrawArrays(GL_LINES)
        geometry->setDrawingMode(QSGGeometry::DrawLines);
        //Add to node
        node->setGeometry(geometry);
        //This setting indicates that the node has ownership of the Geometry instance and will be released when the node is destroyed or reset
        node->setFlag(QSGNode::OwnsGeometry);

        //Construction material, solid color used here
        QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
        //colour
        material->setColor(QColor(255, 0, 0));
        //Add to node
        node->setMaterial(material);
        //This setting indicates that the node owns the ownership of the Material instance and will be released when the node is destroyed or reset
        node->setFlag(QSGNode::OwnsMaterial);
    } else {
        //After initialization, the subsequent refresh will enter this logic
        //Here we can update geometry
        node = static_cast<QSGGeometryNode *>(oldNode);
        geometry = node->geometry();
        //You can reset the number of coordinate points. For example, if you refresh the data and call update, you can reset it here
        //geometry->allocate(2);
    }

    //This function corresponds to the geometry construction parameter, which is equivalent to cast ing a pointer to a memory block
    //If it is a custom geometry structure, just get the pointer directly from geometry - > vertexdata()
    QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
    vertices[0].set(0,0); //Top left corner of point 1
    vertices[1].set(width(),height()); //Lower right corner of point 2
    //After setting the dirty flag, the scene map will refresh the corresponding content
    //If dirty geometry is not set, the data changes will not be refreshed
    //(in this case, the ui rendering is not refreshed after dragging the size)
    node->markDirty(QSGNode::DirtyGeometry);

    return node;
}

The geometry and material classes provided by Qt used above can also be extended by ourselves after inheritance. The following is the second example. (due to the large number of codes, I removed the Item part and only pasted the geometry and material parts,

For the complete code, see: https://github.com/gongjianbo/MyTestCode2021/tree/master/Qml/LearnQSG_20210614_Line)

#pragma once
#include <QSGGeometry>

//Reference source code:
//qt-everywhere-src\qtdeclarative\src\quick\scenegraph\coreapi\qsggeometry.h
//Here is a ColoredPoint2D
class MyGeometry : public QSGGeometry
{
public:
    using QSGGeometry::QSGGeometry;
    //Data structures are defined only for ease of operation
    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"

//The constructor of QSGGeometry requires the AttributeSet parameter
//struct AttributeSet {
//    int count;
//    int stride;
//    const Attribute *attributes;
//};
const QSGGeometry::AttributeSet &MyGeometry::defaultAttributes_My2D()
{
    static Attribute data[] = {
        //Coordinate vertex of corresponding material
        Attribute::createWithAttributeType(0, 2, FloatType, PositionAttribute),
        //Corresponding material color vertex
        Attribute::createWithAttributeType(1, 4, UnsignedByteType, ColorAttribute)
    };
    //count is the number of attributes, and stripe is the vertex size / step
    //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 part demo uses QSGSimpleMaterial, but this class is marked obsolete
//So here we refer to the source code QSGFlatColorMaterial, vertex color material and texture material
//qt-everywhere-src\qtdeclarative\src\quick\scenegraph\util\qsgflatcolormaterial.h
class MyMaterial : public QSGMaterial
{
public:
    MyMaterial();
    //This function is called by the scene graph to return a unique instance of each subclass
    QSGMaterialType *type() const override;
    //For each material type existing in the scene graph, createShader will be called only once and will be cached internally
    QSGMaterialShader *createShader() const override;
    //Compare this material with other materials and return 0 if equal
    //This material is first sorted as - 1, and other materials are first sorted as 1
    //The scene graph can rearrange geometric nodes to minimize state changes.
    //The compare function is called in the sorting process so that the material can be sorted.
    //To minimize state changes every time QSGMaterialShader::updateState() is called.
    int compare(const QSGMaterial *other) const override;

    //Custom interfaces are used to set material properties
    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()
    {
        //Don't bother to add two files and use vertexShader to return
        //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;
})";
    }

    //This function will be called when refreshing to determine what needs to be updated by judging the status
    void updateState(const RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override
    {
        Q_UNUSED(newEffect)
        Q_UNUSED(oldEffect)
        //The QSG mixing mode is glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA), which provides premultiplied mixing
        //FlatColorMaterialShader::updateState pre multiplies transparency, but now I use color as a vertex attribute
        //The glsl is changed to: color = vec4(vertexColor.rgb*vertexColor.a,vertexColor.a) * opacity;
        if (state.isOpacityDirty()){
            //The color change is also set to opacitydirty
            program()->setUniformValue(opacityId, state.opacity());
            //Enable LINE_SMOOTH or MSAA can lead to lateral / vertical straight line fattening
            //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());
    }

    //Vertex attribute name, and nullptr is filled in the last one of the array
    //vertexData data data structure corresponding to geometry
    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;
    //Enable blending with transparency
    setFlag(Blending, fragColor.alpha() != 0xff);
}

 

Topics: Qt qml