Qt Quick Scene Graph 相关文档:https://doc.qt.io/qt-5/qtquick-visualcanvas-scenegraph.html
参照示例 twotextureproviders
本文完整代码链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qml/LearnQSG_20210624_Texture
上一节学习了画线,通过设置顶点参数也可以绘制任意几何图元,这一节学习纹理的基本操作。
回顾下基本知识,一个几何节点 Node 由顶点 Geometry 和材质 Material 组成;QQuickItem 如果是可视类型需要设置 "setFlag(ItemHasContents, true);";Geometry、Material 及其他更新后单独设置 dirty 标志,这样在刷新时判断对应的 dirty 标志可以减少不必要的操作。
如果只是简单的展示一个纹理,如图片等,可以使用 QSGTextureMaterial,基本操作如下:
#pragma once
#include <QQuickItem>
//学习QSGTextureMaterial的基本使用
class MyTextureItem : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QQuickItem *source READ getSource WRITE setSource NOTIFY sourceChanged)
public:
explicit MyTextureItem(QQuickItem * parent = nullptr);
//设置提供纹理的item
QQuickItem *getSource() const { return source; }
void setSource(QQuickItem *item);
protected:
//更新渲染节点时调用
QSGNode *updatePaintNode(QSGNode *oldNode,
QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;
signals:
void sourceChanged();
private:
//提供纹理的对象
QQuickItem *source = nullptr;
//变更source后需要重置texture
bool sourceChange = false;
};
#include "MyTextureItem.h"
#include <QSGGeometryNode>
#include <QSGGeometry>
#include <QSGMaterial>
#include <QSGTextureProvider>
#include <QSGTextureMaterial>
MyTextureItem::MyTextureItem(QQuickItem *parent)
: QQuickItem(parent)
{
//要渲染的节点需要设置此标志
setFlag(ItemHasContents, true);
}
void MyTextureItem::setSource(QQuickItem *item)
{
if(source == item)
return;
source = item;
//变更source后需要重置texture
sourceChange = true;
emit sourceChanged();
update();
}
QSGNode *MyTextureItem::updatePaintNode(QSGNode *oldNode,
QQuickItem::UpdatePaintNodeData *updatePaintNodeData)
{
//基本逻辑copy自Qt那几个scene graph的示例
Q_UNUSED(updatePaintNodeData)
bool abort = false;
//未设置纹理源或者该Item不能创建纹理,就不显示
if (!source || !source->isTextureProvider()) {
qDebug() << "source is null or not a texture provider";
abort = true;
}
if (abort) {
delete oldNode;
return nullptr;
}
//一个node包含geometry和material,类似于别的架构中的entity、mesh、material
//node可以是要渲染的物体,也可以是透明度等
//geometry定义了网格、顶点、结构等,比如坐标点
//material定义了填充方式
QSGGeometryNode *node = nullptr;
QSGGeometry *geometry = nullptr;
if (!oldNode) {
//如果只是显示基本的线和纹理,用一个QSGGeometryNode即可
node = new QSGGeometryNode;
//使用纹理顶点,4个表示四个角
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
//绘制模式,设置为连续三角,这样只用四个点就可以表示一个矩形
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); //GL_TRIANGLE_STRIP
//添加到node
node->setGeometry(geometry);
//此设置表示该节点拥有Geometry实例的所有权,并且会在该节点被销毁或重新设置时将其释放
node->setFlag(QSGNode::OwnsGeometry);
//创建纹理
QSGTextureMaterial *material = new QSGTextureMaterial;
//设置纹理数据,同一个item的纹理内容更新了会自动触发更新
//但是更换item后需要重新设置texture
material->setTexture(source->textureProvider()->texture());
//添加到node
node->setMaterial(material);
//此设置表示该节点拥有Material实例的所有权,并且会在该节点被销毁或重新设置时将其释放
node->setFlag(QSGNode::OwnsMaterial);
} else {
node = static_cast<QSGGeometryNode *>(oldNode);
geometry = node->geometry();
//如果item重置了就重新设置纹理
if(sourceChange) {
sourceChange = false;
QSGTextureMaterial *material = static_cast<QSGTextureMaterial*>(node->material());
material->setTexture(source->textureProvider()->texture());
}
}
QSGGeometry::TexturedPoint2D *vertices = geometry->vertexDataAsTexturedPoint2D();
//四个顶点,每个顶点为屏幕坐标xy+纹理坐标xy
//opengl纹理坐标默认左下角为0点,但是QSG默认左上角为0点
//(注意更新纹理的时候,width和height也要绑定更新)
vertices[0].set(0,0,0,0);
vertices[1].set(0,0+height(),0,1);
vertices[2].set(0+width(),0,1,0);
vertices[3].set(0+width(),0+height(),1,1);
//效果同上
//QSGGeometry::updateTexturedRectGeometry(geometry, boundingRect(), QRectF(0, 0, 1, 1));
//标记为dirty后才会刷新节点对应内容
node->markDirty(QSGNode::DirtyGeometry);
node->markDirty(QSGNode::DirtyMaterial);
return node;
}
MyTextureItem{
id: texture
width: source.width
height: source.height
source: txt
}
Qt 提供的基本类型只给了单个纹理设置的接口,如果需要多个纹理同时处理则可以参照示例:twotextureproviders
但是这个示例有点小问题,很多版本都有改进该示例(如 5.12、5.15、5.1 代码都有差异),所以代码有差异。由于老的示例使用了废弃的接口,而新的示例又使用了 Qt RHI 的接口,所以我选择了 5.15 的示例进行学习。
这里有个小插曲就是我觉得示例这个棋盘背景挺有意思的,设计巧妙,所以复制粘贴过来了。
ShaderEffect {
id: bg
anchors.fill: parent
property real tileSize: 16
property color color1: Qt.rgba(0.9, 0.9, 0.9, 1);
property color color2: Qt.rgba(0.8, 0.8, 0.8, 1);
property size pixelSize: Qt.size(width / tileSize, height / tileSize);
fragmentShader:
"
uniform lowp vec4 color1;
uniform lowp vec4 color2;
uniform highp vec2 pixelSize;
varying highp vec2 qt_TexCoord0;
void main() {
highp vec2 tc = sign(sin(3.14159265358979323846 * qt_TexCoord0 * pixelSize));
if (tc.x != tc.y)
gl_FragColor = color1;
else
gl_FragColor = color2;
}
"
}
下面进入正题。
首先是继承 QQuickItem 然后重写 updatePaintNode 接口,在接口内实例化了一个自定义的 Node 对象,而这个 Node 对象的功能就是将两个纹理进行异或操作,都有颜色的地方变为透明。
Node 设置了 setFlag(QSGNode::UsePreprocess, true),在每次刷新的时候会回调 preprocess 接口,我们就在该接口内更新纹理。Node 的顶点使用基本类型 QSGGeometry,注意顶点参数为 4 ,GL_TRIANGLE_STRIP 时表示两个连续的三角,一般组合为矩形,纹理就在这个矩形范围内渲染:
//4个顶点表示绘制一个矩形,使用GL_TRIANGLE_STRIP
m_node.setGeometry(new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4));
m_node.setFlag(QSGNode::OwnsGeometry);
//顶点值在改变rect后才设置
QSGGeometry::updateTexturedRectGeometry(m_node.geometry(), m_rect, QRectF(0, 0, 1, 1));
Node 的材质使用的自定义类型,以支持两个纹理的导入。这里面主要是给材质自定义了一个 Shader 类,在初始化时创建并绑定相应的 GL 属性:
void XorBlendShader::initialize()
{
m_matrix_id = program()->uniformLocation("qt_Matrix");
m_opacity_id = program()->uniformLocation("qt_Opacity");
//相当于绑定到了GL_TEXTURE0上,后面设置纹理数据直接操作GL_TEXTURE0
program()->setUniformValue("uSource1", 0); // GL_TEXTURE0
program()->setUniformValue("uSource2", 1); // GL_TEXTURE1
}
然后刷新的时候将纹理 bind 并进行渲染:
void XorBlendShader::updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect)
{
XorBlendMaterial *material = static_cast<XorBlendMaterial *>(newEffect);
if (state.isMatrixDirty())
program()->setUniformValue(m_matrix_id, state.combinedMatrix());
if (state.isOpacityDirty())
program()->setUniformValue(m_opacity_id, state.opacity());
QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
//先active再bind,步骤和使用Qt封装的OpenGL使用方式一样
f->glActiveTexture(GL_TEXTURE1);
material->state.texture2->bind();
f->glActiveTexture(GL_TEXTURE0);
material->state.texture1->bind();
}
如果你熟悉 Qt 封装的 OpenGL 类,会感觉很熟悉,使用方式基本差不多。知道了流程后,我们就可以扩展为任意个纹理的组合。
最后再学习下示例异或纹理显示的片段着色器代码,取出该纹理坐标像素的颜色后根据透明度来处理,带入计算得,p1 透明 p2 不透明则显示 p2,p1 p2 都不透明则透明度为 0 都不显示,如果 p1 p2 都透明则透明度也为 0 都不显示。
uniform lowp float qt_Opacity;
uniform lowp sampler2D uSource1;
uniform lowp sampler2D uSource2;
varying highp vec2 vTexCoord;
void main()
{
lowp vec4 p1 = texture2D(uSource1, vTexCoord);
lowp vec4 p2 = texture2D(uSource2, vTexCoord);
gl_FragColor = (p1 * (1.0 - p2.a) + p2 * (1.0 - p1.a)) * qt_Opacity;
}