手搓一个带顶点色的QSG自定义Material:从GLSL到Qt Quick渲染管线的完整指南

张开发
2026/4/17 10:45:31 15 分钟阅读

分享文章

手搓一个带顶点色的QSG自定义Material:从GLSL到Qt Quick渲染管线的完整指南
手搓一个带顶点色的QSG自定义Material从GLSL到Qt Quick渲染管线的完整指南在Qt Quick的世界里Scene Graph场景图是渲染引擎的核心架构。对于需要高性能图形渲染的场景直接操作QSG节点和材质往往能带来更精细的控制和更好的性能表现。本文将带你从零开始实现一个支持顶点颜色Vertex Color的自定义Material深入剖析QSG渲染管线的每个环节。1. QSG渲染管线基础Qt Quick Scene Graph采用基于节点的树状结构来描述UI元素每个可视元素对应一个QSGNode。当我们需要自定义渲染时通常需要关注以下几个核心类QSGGeometryNode包含几何数据顶点、索引和材质信息QSGGeometry定义顶点数据的结构和内容QSGMaterial描述物体表面的外观特性QSGMaterialShader实现实际的GLSL着色器代码与传统的QPainter方式不同QSG直接工作在OpenGL层面这要求我们对图形管线有基本了解。一个典型的渲染流程包括顶点着色器处理几何数据图元装配和光栅化片段着色器计算最终像素颜色混合输出到帧缓冲区提示在QSG中所有渲染操作都在专门的渲染线程执行这意味着我们必须在主线程准备好所有渲染数据。2. 自定义Geometry实现要实现顶点颜色功能首先需要定义能够携带颜色信息的顶点结构。Qt提供了QSGGeometry::defaultAttributes_ColoredPoint2D()但为了更深入理解原理我们选择自己实现class ColoredGeometry : public QSGGeometry { public: struct Vertex { float x, y; uchar r, g, b, a; void set(float nx, float ny, QColor color) { x nx; y ny; r color.red(); g color.green(); b color.blue(); a color.alpha(); } }; static const QSGGeometry::AttributeSet attributes() { static QSGGeometry::Attribute attr[] { QSGGeometry::Attribute::create(0, 2, GL_FLOAT, true), // 位置 QSGGeometry::Attribute::create(1, 4, GL_UNSIGNED_BYTE, false) // 颜色 }; static QSGGeometry::AttributeSet set { 2, sizeof(Vertex), attr }; return set; } ColoredGeometry(int vertexCount) : QSGGeometry(attributes(), vertexCount) {} Vertex *vertexData() { return static_castVertex*(vertexData()); } };这个自定义Geometry类具有以下特点每个顶点包含位置(x,y)和颜色(r,g,b,a)信息使用GL_FLOAT存储位置GL_UNSIGNED_BYTE存储颜色分量提供了便捷的set方法用于设置顶点属性3. 实现自定义MaterialQSGMaterial负责定义物体表面的外观特性。对于支持顶点颜色的材质我们需要继承QSGMaterial并实现必要接口提供对应的着色器程序处理材质状态更新class VertexColorMaterial : public QSGMaterial { public: QSGMaterialType *type() const override { static QSGMaterialType type; return type; } QSGMaterialShader *createShader() const override { return new VertexColorShader; } int compare(const QSGMaterial *other) const override { return 0; // 简单实现所有实例视为相同 } }; class VertexColorShader : public QSGMaterialShader { public: 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 vertexColor * opacity; } ); } const char *fragmentShader() const override { return R( varying lowp vec4 color; void main() { gl_FragColor color; } ); } char const *const *attributeNames() const override { static const char *names[] { vertexCoord, vertexColor, 0 }; return names; } void initialize() override { QSGMaterialShader::initialize(); m_matrix_id program()-uniformLocation(matrix); m_opacity_id program()-uniformLocation(opacity); } void updateState(const RenderState state, QSGMaterial *, QSGMaterial *) override { if (state.isMatrixDirty()) program()-setUniformValue(m_matrix_id, state.combinedMatrix()); if (state.isOpacityDirty()) program()-setUniformValue(m_opacity_id, state.opacity()); } private: int m_matrix_id; int m_opacity_id; };着色器代码中的关键点vertexCoord和vertexColor对应Geometry中的顶点属性matrix是QSG自动提供的模型视图投影矩阵opacity处理节点的透明度颜色值在顶点着色器中计算后通过varying传递给片段着色器4. 整合到QQuickItem最后我们需要将这些组件整合到一个自定义QQuickItem中class ColoredShape : public QQuickItem { Q_OBJECT public: ColoredShape(QQuickItem *parent nullptr) : QQuickItem(parent) { setFlag(ItemHasContents); } protected: QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override { QSGGeometryNode *node static_castQSGGeometryNode*(oldNode); if (!node) { node new QSGGeometryNode; auto *geometry new ColoredGeometry(4); auto *material new VertexColorMaterial; node-setGeometry(geometry); node-setMaterial(material); node-setFlag(QSGNode::OwnsGeometry); node-setFlag(QSGNode::OwnsMaterial); // 设置顶点数据 auto *v geometry-vertexData(); v[0].set(0, 0, Qt::red); v[1].set(width(), 0, Qt::green); v[2].set(width(), height(), Qt::blue); v[3].set(0, height(), Qt::yellow); geometry-setDrawingMode(GL_TRIANGLE_FAN); } else { // 更新已有节点的几何数据 auto *geometry node-geometry(); auto *v static_castColoredGeometry*(geometry)-vertexData(); v[1].x width(); v[2].x width(); v[2].y height(); v[3].y height(); node-markDirty(QSGNode::DirtyGeometry); } return node; } };关键实现细节ItemHasContents标志必须设置这个标志才能启用自定义渲染节点所有权通过OwnsGeometry和OwnsMaterial标志管理资源生命周期脏标记数据变更后必须调用markDirty()通知渲染线程绘制模式支持GL_TRIANGLES、GL_LINES等多种图元类型5. 性能优化与高级技巧在实际项目中我们还需要考虑以下优化点5.1 批处理渲染QSG会自动合并使用相同材质的几何节点为了最大化批处理效果尽量复用材质实例避免频繁修改材质状态对静态内容使用QSGGeometry::StaticDrawinggeometry-setDrawingMode(GL_TRIANGLES); geometry-setVertexDataPattern(QSGGeometry::StaticPattern);5.2 着色器优化现代GLSL支持更多优化特性// 使用精度限定符减少功耗 attribute highp vec4 position; varying mediump vec4 color; // 使用内置函数优化计算 gl_Position matrix * (position * vec4(1.0, 1.0, 1.0, 1.0));5.3 动态几何更新对于频繁变动的几何数据考虑以下策略策略适用场景实现方式CPU更新少量顶点直接修改vertexDataGPU实例化大量相似对象使用实例化数组计算着色器复杂变形GLSL compute shader6. 调试与问题排查QSG渲染问题通常表现为黑屏或图形异常常用调试手段包括检查OpenGL错误GLenum err; while ((err glGetError()) ! GL_NO_ERROR) { qDebug() OpenGL error: err; }验证着色器编译if (!program()-link()) { qWarning() Shader link error: program()-log(); }使用调试工具Qt Creator的Scene Graph调试视图OpenGL调试器如RenderDoc打印顶点数据验证内容在实现自定义Material的过程中最常见的陷阱包括顶点属性定义与着色器不匹配忘记设置Dirty标记导致更新不生效资源管理不当造成内存泄漏跨线程访问OpenGL资源7. 实际应用案例让我们看一个更复杂的例子实现一个渐变颜色的圆环。这个例子展示了如何动态生成几何数据使用顶点颜色实现平滑渐变处理抗锯齿边缘QSGNode *GradientRing::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { const int segments 64; QSGGeometryNode *node static_castQSGGeometryNode*(oldNode); if (!node) { node new QSGGeometryNode; auto *geometry new ColoredGeometry(segments * 2); auto *material new VertexColorMaterial; node-setGeometry(geometry); node-setMaterial(material); node-setFlag(QSGNode::OwnsGeometry); node-setFlag(QSGNode::OwnsMaterial); } auto *geometry static_castColoredGeometry*(node-geometry()); geometry-allocate(segments * 2); const float outer width() / 2; const float inner outer * 0.7f; auto *v geometry-vertexData(); for (int i 0; i segments; i) { float angle 2 * M_PI * i / segments; float x cosf(angle); float y sinf(angle); QColor outerColor QColor::fromHsvF(i / float(segments), 1, 1); QColor innerColor outerColor.darker(150); v[i*2].set(outer outer*x, outer outer*y, outerColor); v[i*21].set(outer inner*x, outer inner*y, innerColor); } geometry-setDrawingMode(GL_TRIANGLE_STRIP); node-markDirty(QSGNode::DirtyGeometry); return node; }这个实现展示了QSG的强大之处通过精细控制每个顶点的位置和颜色我们可以创建出高度定制化的视觉效果同时保持高效的渲染性能。

更多文章