编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

      • QML基础入门
      • 嵌入式GUI技术全景
      • QML引擎与渲染原理
      • QML语法与类型系统
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
      • Canvas与自定义渲染
      • QML与C++集成原理
      • 自定义SceneGraph节点
        • 11.1 案例引入
          • 11.1.1 预览掉帧
          • 11.1.2 根因分析
          • 11.1.3 三大问题
        • 11.2 节点体系
          • 11.2.1 类型生命周期
          • 11.2.2 子节点挂载
          • 11.2.3 节点变换
          • 11.2.4 渲染四阶段
        • 11.3 自定义全流程
          • 11.3.1 复用与资源
          • 11.3.2 顶点缓冲深入
          • 11.3.3 频谱柱状图
        • 11.4 材质着色器
          • 11.4.1 QSGMaterial 体系
          • 11.4.2 着色器编写
          • 11.4.3 uniform 批处理
          • 11.4.4 发光矩形
          • 11.4.5 批处理断点
          • 11.4.6 内置材质选用决策
        • 11.5 零拷贝纹理
          • 11.5.1 原生纹理
          • 11.5.2 摄像头链路
          • 11.5.3 摄像头预览
        • 11.6 粒子系统
        • 11.7 线程与脏标
        • 11.8 新手陷阱
        • 11.9 训练题
        • 11.10 速查表
      • 交叉编译与部署
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QML基础入门
杨充
2025-06-24
目录

自定义SceneGraph节点

# 第 11 章 自定义 SceneGraph 节点

本章定位:直接操控 GPU 渲染的最后一层。前面第 9 章讲了三条自定义渲染路径——Canvas(JS → FBO)、QQuickPaintedItem(QPainter → FBO)、QQuickItem+QSGNode(顶点缓冲)。本章深入第三条路径的最深处:自定义 QSGMaterial 材质系统、自写着色器(Vertex/Fragment Shader)、以及摄像头帧的零拷贝纹理上传——这是 QML 渲染性能的天花板,也是嵌入式上跑 60fps 复杂视觉效果的唯一解。

# 目录介绍

  • 11.1 案例引入
    • 11.1.1 预览掉帧
    • 11.1.2 根因分析
    • 11.1.3 三大问题
  • 11.2 节点体系
    • 11.2.1 类型生命周期
    • 11.2.2 子节点挂载
    • 11.2.3 节点变换
  • 11.3 自定义全流程
    • 11.3.1 复用与资源
    • 11.3.2 顶点缓冲深入
    • 11.3.3 频谱柱状图
  • 11.4 材质着色器
    • 11.4.1 QSGMaterial 体系
    • 11.4.2 着色器编写
    • 11.4.3 uniform 批处理
    • 11.4.4 发光矩形
  • 11.5 零拷贝纹理
    • 11.5.1 原生纹理
    • 11.5.2 摄像头链路
    • 11.5.3 摄像头预览
  • 11.6 粒子系统
  • 11.7 线程与脏标
  • 11.8 新手陷阱
  • 11.9 训练题
  • 11.10 速查表

# 11.1 案例引入

# 11.1.1 预览掉帧

某车载工程师用 VideoOutput 显示后视摄像头画面——ARM Cortex-A53 + Mali-G31 板上预览帧率 8fps,延迟超过 200ms:

VideoOutput {
    source: camera
    anchors.fill: parent
}

排查发现 VideoOutput 内部的渲染路径:

摄像头硬件 → V4L2 驱动 → 内核 DMA buffer
  → Qt Multimedia → QVideoFrame
    → QVideoFrame::toImage()          ⚠️ CPU 拷贝!
      → glTexImage2D(QImage)          ⚠️ 纹理上传!
        → QSGGeometryNode 渲染

每一帧都经历了 GPU → CPU(map 内存)→ CPU(格式转换)→ CPU → GPU(texImage2D) 四次跨边界拷贝。

# 11.1.2 根因分析

修复方案——绕过 VideoOutput,直接用 QQuickItem + createTextureFromNativeObject 把 DMA buffer 的 GPU 纹理句柄传给 Scene Graph:

摄像头硬件 → V4L2 → DMA buffer
  → eglCreateImageKHR → GLuint texture  ← 零 CPU 拷贝!直接 GPU 纹理
    → window()->createTextureFromNativeObject(nativeHandle)
      → QSGTextureMaterial → QSGGeometryNode → 渲染

结果:ARM 板上 8fps → 30fps(摄像头硬件帧率上限),延迟从 200ms 降到 < 16ms。

# 11.1.3 三大问题

问题 在哪节回答
QSGGeometryNode + QSGFlatColorMaterial 之外,自定义材质怎么写? §11.4
GPU 纹理句柄如何不经过 CPU 直接传给 QML? §11.5
自定义节点在 Render Thread 内如何保证线程安全? §11.7

# 11.2 节点体系

# 11.2.1 类型生命周期

QSGNode ── 所有节点的基类
  ├── QSGTransformNode ── 变换矩阵(translate/rotate/scale)
  ├── QSGOpacityNode   ── 透明度
  ├── QSGClipNode      ── 裁剪(scissor test)
  └── QSGGeometryNode  ── 核心绘制节点(顶点缓冲 + 材质)
       ├── QSGGeometry       ── 顶点数据(坐标/颜色/纹理坐标)
       └── QSGMaterial       ── 材质(着色器 + uniform + 纹理)

节点的创建与销毁:

// updatePaintNode 被 Render Thread 的 Sync 阶段调用
QSGNode* MyItem::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
    auto* node = static_cast<QSGGeometryNode*>(oldNode);

    // ① 首次调用:oldNode == nullptr → 创建新节点
    if (!node) {
        node = new QSGGeometryNode();
        auto* geom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 100);
        node->setGeometry(geom);
        node->setFlag(QSGNode::OwnsGeometry);      // node 析构时释放 geometry

        auto* mat = new QSGFlatColorMaterial();
        mat->setColor(Qt::green);
        node->setMaterial(mat);
        node->setFlag(QSGNode::OwnsMaterial);      // node 析构时释放 material
    }

    // ② 后续调用:复用 oldNode,只更新数据——避免每帧 new/delete
    auto* geom = node->geometry();
    updateVertices(geom->vertexDataAsPoint2D());

    // ③ 标记脏位——告知 Scene Graph 需要更新什么
    node->markDirty(QSGNode::DirtyGeometry);

    return node;
}

OwnsGeometry / OwnsMaterial 的含义——当 QSGNode 被 Scene Graph 销毁时,自动释放挂载的 geometry 和 material:

标志 效果
OwnsGeometry node 析构时 delete geometry
OwnsMaterial node 析构时 delete material
OwnedByParent node 被父节点析构时一并销毁
UsePreprocess 本节点的 preprocess() 会被调用

# 11.2.2 子节点挂载

QSGNode 可以有子节点——形成渲染树:

QSGNode* MyItem::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
    auto* root = static_cast<QSGNode*>(oldNode);
    if (!root) {
        root = new QSGNode();                          // 根——无视觉

        auto* bg = new QSGGeometryNode();              // 背景矩形
        // ... 设置 bg 的 geometry + material
        root->appendChildNode(bg);

        auto* label = new QSGGeometryNode();           // 文字标签
        // ... 设置 label 的 geometry + material
        root->appendChildNode(label);
    }
    return root;
}

节点层级与渲染顺序:

Root (QSGNode)
  ├── Background (QSGGeometryNode)            ← 先画
  ├── GridLines (QSGGeometryNode)             ← 再画
  ├── DataCurve (QSGGeometryNode)             ← 再画
  └── Label (QSGGeometryNode)                  ← 最后画(最顶层)

Scene Graph 在 Render Phase 按前序遍历渲染——
不依赖 z 值(那是 QML Item 层面的东西)。

# 11.2.3 节点变换

如果节点需要在渲染前计算变换矩阵(如动态缩放),重写 preprocess():

class ScaledNode : public QSGGeometryNode {
public:
    void preprocess() override {
        // 在 Render Phase 之前——计算变换矩阵
        QMatrix4x4 m;
        m.scale(m_scale);
        setMatrix(m);
        QSGGeometryNode::preprocess();   // 调用父类
    }

    void setScale(float s) {
        if (qFuzzyCompare(s, m_scale)) return;
        m_scale = s;
        markDirty(QSGNode::DirtyMatrix);
    }

private:
    float m_scale = 1.0f;
};

# 11.2.4 渲染四阶段

updatePaintNode 并非每帧调用一次——它在 Scene Graph 内部时序有固定的"入口窗口":

GUI 线程                          Render Thread
══════════                         ════════════════
1. 属性变更:
   setProperty("width", 200)
   → QQuickItem::polish() 排队

2. 信号到达 Tick:
   → QQuickItem::update()
   → item 标记 dirty

─────────────────────────────────────────────────
3. Sync阶段(阻塞—挂起GUI线程)
   遍历所有 dirty QuickItem:
     item->updatePaintNode(oldNode, ...)
                          ↑ 你写的代码
     → 读GUI线程属性(安全—GUI线程已暂停)
     → 写 QSGNode 数据(安全—Render Thread独占)
   return newRoot;

   新旧 QSGNode 树 diff:
     → 回收旧节点(触发 ~QSGNode 析构链)
     → 挂载新节点

   GUI线程恢复

4. Preprocess阶段
   遍历 QSGNode 树(前序):
     node->preprocess()
       → 计算变换矩阵
       → 更新动态状态
     ← UsePreprocess flag必须设置

5. Render阶段
   前序遍历 QSGNode 树:
     → 对每个 QSGGeometryNode:
       ① 检查 DirtyGeometry → 上传顶点缓冲到GPU
       ② 检查 DirtyMaterial  → 调用 updateUniformData
       ③ 绑定着色器
       ④ glDrawArrays
     → 批处理排序(compare分组+合并)
     → eglSwapBuffers

关键——updatePaintNode 在 Sync 阶段:可安全读 width()/color() 等属性,但不能调 window()——window() 在 Preprocess/Render 阶段才可用。

updatePaintNode vs preprocess 分工:

操作 updatePaintNode(Sync) preprocess(Render前)
读取 QQuickItem 属性 ✅ ⚠️ 不推荐
创建/修改 QSGNode ✅ ✅
调用 window() ❌ ✅
创建纹理 ❌ ✅
计算变换矩阵 ⚠️ 可以 ✅ 设计目的

# 11.3 自定义全流程

# 11.3.1 复用与资源

class BarChartItem : public QQuickItem {
    Q_OBJECT
    Q_PROPERTY(QVector<double> values READ values WRITE setValues NOTIFY valuesChanged)

public:
    QVector<double> values() const { return m_values; }
    void setValues(const QVector<double>& v) {
        m_values = v;
        emit valuesChanged();
        update();                // ← 标记 QQuickItem dirty → 触发 updatePaintNode
    }

signals:
    void valuesChanged();

protected:
    QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
        auto* root = static_cast<QSGNode*>(oldNode);
        if (!root) {
            root = new QSGNode();     // 复用整个子树——不要每帧重建

            for (int i = 0; i < MAX_BARS; ++i) {
                auto* bar = new QSGGeometryNode();
                auto* geom = new QSGGeometry(
                    QSGGeometry::defaultAttributes_Point2D(), 4);
                geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
                bar->setGeometry(geom);
                bar->setFlag(QSGNode::OwnsGeometry);

                auto* mat = new QSGFlatColorMaterial();
                bar->setMaterial(mat);
                bar->setFlag(QSGNode::OwnsMaterial);

                root->appendChildNode(bar);   // 预创建全部 bar 节点
            }
        }

        // 每帧只更新顶点数据——不创建新节点
        for (int i = 0; i < m_values.size() && i < MAX_BARS; ++i) {
            auto* bar = static_cast<QSGGeometryNode*>(root->childAtIndex(i));
            updateBarGeometry(bar->geometry(), i, m_values[i]);
            bar->markDirty(QSGNode::DirtyGeometry);
        }

        return root;
    }

private:
    QVector<double> m_values;
    static constexpr int MAX_BARS = 64;
};

# 11.3.2 顶点缓冲深入

四种顶点属性集:

// ① Point2D —— 仅 (x, y),适合纯顶点位置
QSGGeometry::defaultAttributes_Point2D()
// 顶点大小: 2 × float = 8 bytes

// ② ColoredPoint2D —— (x, y, r, g, b, a),适合逐顶点颜色
QSGGeometry::defaultAttributes_ColoredPoint2D()
// 顶点大小: 6 × float = 24 bytes

// ③ TexturedPoint2D —— (x, y, tx, ty),适合纹理贴图
QSGGeometry::defaultAttributes_TexturedPoint2D()
// 顶点大小: 4 × float = 16 bytes

// ④ 自定义属性集
QSGGeometry::AttributeSet custom = {
    3,                              // 属性数量
    sizeof(MyVertex),               // 顶点大小
    {
        QSGGeometry::Attribute::createWithAttributeType(0, 2,
            QSGGeometry::FloatType,  QSGGeometry::PositionAttribute),
        QSGGeometry::Attribute::createWithAttributeType(2, 4,
            QSGGeometry::FloatType,  QSGGeometry::ColorAttribute),
        QSGGeometry::Attribute::createWithAttributeType(6, 2,
            QSGGeometry::FloatType,  QSGGeometry::TexCoordAttribute),
    }
};

自定义顶点结构:

struct Vertex3D {
    float x, y, z;        // 位置 (offset 0)
    float r, g, b, a;     // 颜色 (offset 12)
    float u, v;           // 纹理坐标 (offset 28)
};
// sizeof(Vertex3D) = 36 bytes

# 11.3.3 频谱柱状图

class SpectrumBars : public QQuickItem {
    Q_OBJECT
    static constexpr int BAR_COUNT = 64;
    static constexpr float BAR_WIDTH = 8.0f;
    static constexpr float BAR_GAP = 2.0f;

    QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
        auto* root = static_cast<QSGNode*>(oldNode);

        // 首次——预创建全部 64 个 bar
        if (!root) {
            root = new QSGNode();
            for (int i = 0; i < BAR_COUNT; ++i) {
                auto* bar = new QSGGeometryNode();

                auto* geom = new QSGGeometry(
                    QSGGeometry::defaultAttributes_ColoredPoint2D(), 4);
                geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
                bar->setGeometry(geom);
                bar->setFlag(QSGNode::OwnsGeometry);

                auto* mat = new QSGFlatColorMaterial();
                bar->setMaterial(mat);
                bar->setFlag(QSGNode::OwnsMaterial);

                root->appendChildNode(bar);
            }
        }

        // 每帧更新顶点 + 颜色
        float w = width();
        float h = height();
        float step = (BAR_WIDTH + BAR_GAP);

        for (int i = 0; i < BAR_COUNT; ++i) {
            float val = (i < m_data.size()) ? m_data[i] : 0.0f;
            float barH = val * h;
            float left = i * step;
            float right = left + BAR_WIDTH;
            float hue = (float)i / BAR_COUNT * 360.0f;
            QColor c = QColor::fromHsv(hue, 200, 255);

            auto* bar = static_cast<QSGGeometryNode*>(root->childAtIndex(i));
            auto* geom = bar->geometry();
            auto* verts = geom->vertexDataAsColoredPoint2D();

            // 三角形带——4 个顶点画一个矩形
            verts[0].set(left,  h, c.redF(), c.greenF(), c.blueF(), 1.0f);
            verts[1].set(left,  h - barH, c.redF(), c.greenF(), c.blueF(), 1.0f);
            verts[2].set(right, h, c.redF(), c.greenF(), c.blueF(), 1.0f);
            verts[3].set(right, h - barH, c.redF(), c.greenF(), c.blueF(), 1.0f);

            bar->markDirty(QSGNode::DirtyGeometry);
        }

        return root;
    }

public slots:
    void pushData(const QVector<float>& data) {
        m_data = data;
        update();
    }

private:
    QVector<float> m_data;
};
SpectrumBars {
    id: spectrum; anchors.fill: parent
    Timer { interval: 50; repeat: true; running: true
        onTriggered: { var d=[]; for(var i=0;i<64;i++) d.push(Math.random()); spectrum.pushData(d) }
    }
}

案例知识融合:本案例演示了 QSGNode 复用的标准模式——①首次调用预创建全部 64 个 bar 节点(含 geometry + material);②后续帧只更新顶点数据(不 new/delete);③用 ColoredPoint2D 属性集实现逐顶点着色(避免创建 64 个不同 material);④所有顶点数据直接在 GPU 缓冲里写入——零拷贝路径。

思考题:

  1. 如果用 64 个 QSGFlatColorMaterial(每个不同色)代替 ColoredPoint2D,Scene Graph 的批处理会发生什么?
  2. verts[0].set(left, h, r, g, b, a) 里的颜色值是 0~255 还是 0.0~1.0?查阅 Qt 文档确认。

# 11.4 材质着色器

# 11.4.1 QSGMaterial 体系

class GlowMaterial : public QSGMaterial {
public:
    GlowMaterial() { setFlag(Blending, true); }    // 开启 alpha 混合

    // ① 材质类型——用于批处理分组
    QSGMaterialType* type() const override {
        static QSGMaterialType t;
        return &t;
    }

    // ② 创建对应着色器
    QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override {
        return new GlowShader();
    }

    // ③ compare——同材质才能合并批处理
    int compare(const QSGMaterial* other) const override {
        auto* o = static_cast<const GlowMaterial*>(other);
        if (m_glowColor != o->m_glowColor) return int(m_glowColor.rgba64() - o->m_glowColor.rgba64());
        if (!qFuzzyCompare(m_glowRadius, o->m_glowRadius)) return m_glowRadius > o->m_glowRadius ? 1 : -1;
        return 0;  // 相同
    }

    QColor glowColor() const { return m_glowColor; }
    void setGlowColor(QColor c) { m_glowColor = c; }

    float glowRadius() const { return m_glowRadius; }
    void setGlowRadius(float r) { m_glowRadius = r; }

private:
    QColor m_glowColor = Qt::white;
    float m_glowRadius = 1.0f;
};

compare() 的核心作用——Scene Graph 在批处理排序时调用 compare() 判断两个材质是否相同:

compare(A, B) == 0 → 相同材质 → 可以合批 ← 减少 Draw Call
compare(A, B) != 0 → 不同材质 → 不能合批

# 11.4.2 着色器编写

class GlowShader : public QSGMaterialShader {
public:
    // Qt 6 新版接口——两个单独的方法
    bool updateUniformData(RenderState& state, QSGMaterial* newMat,
                           QSGMaterial* oldMat) override
    {
        auto* mat = static_cast<GlowMaterial*>(newMat);

        // 仅在 uniform 值变化时才更新
        bool changed = false;
        QByteArray* buf = state.uniformData();
        Q_ASSERT(buf->size() >= 36);

        if (!oldMat || mat->glowColor() != static_cast<GlowMaterial*>(oldMat)->glowColor()
            || state.isMatrixDirty()) {
            QMatrix4x4 mvp = state.combinedMatrix();
            memcpy(buf->data(), mvp.constData(), 64);          // MVP 矩阵
            changed = true;
        }

        if (!oldMat || mat->glowColor() != static_cast<GlowMaterial*>(oldMat)->glowColor()) {
            float r = mat->glowColor().redF();
            float g = mat->glowColor().greenF();
            float b = mat->glowColor().blueF();
            memcpy(buf->data() + 64, &r, 4);
            memcpy(buf->data() + 68, &g, 4);
            memcpy(buf->data() + 72, &b, 4);
            changed = true;
        }

        if (!oldMat || mat->glowRadius() != static_cast<GlowMaterial*>(oldMat)->glowRadius()) {
            float r = mat->glowRadius();
            memcpy(buf->data() + 76, &r, 4);
            changed = true;
        }

        return changed;
    }

    // ② 加载预编译着色器(qt_add_shaders 构建系统)
    void setShaderFiles(ShaderType type, QStringList& files) override {
        if (type == VertexShader)
            files << QStringLiteral(":/shaders/glow.vert.qsb");
        else if (type == FragmentShader)
            files << QStringLiteral(":/shaders/glow.frag.qsb");
    }
};

GLSL 着色器源码(构建时编译为 .qsb):

// glow.vert — 顶点着色器
#version 440
layout(location = 0) in vec4 vertexCoord;
layout(std140, binding = 0) uniform buf {
    mat4 matrix;
    vec4 glowColor;
    float glowRadius;
} ubuf;

out gl_PerVertex { vec4 gl_Position; };
out vec2 texCoord;

void main() {
    gl_Position = ubuf.matrix * vertexCoord;
}

// glow.frag — 片元着色器
#version 440
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
    mat4 matrix;
    vec4 glowColor;
    float glowRadius;
} ubuf;

void main() {
    // 简单发光效果:向外衰减
    vec2 uv = gl_FragCoord.xy / vec2(800.0, 480.0);
    float dist = length(uv - 0.5);
    float glow = exp(-dist * ubuf.glowRadius);
    fragColor = ubuf.glowColor * glow;
}

# 11.4.3 uniform 批处理

uniform 更新的优化策略——利用 oldMat 参数跳过未变化的字段:

bool updateUniformData(RenderState& state, QSGMaterial* newMat,
                       QSGMaterial* oldMat) override
{
    bool changed = false;

    // 仅在 oldMat == nullptr(首次)或值变化时更新
    if (!oldMat || mat->glowRadius() != static_cast<GlowMaterial*>(oldMat)->glowRadius()) {
        // 更新 uniform
        changed = true;
    }

    return changed;  // false → Scene Graph 跳过本次 uniform 上传
}

# 11.4.4 发光矩形

class GlowRectangle : public QQuickItem {
    Q_OBJECT
    Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged)
    Q_PROPERTY(float glowRadius READ glowRadius WRITE setGlowRadius NOTIFY glowRadiusChanged)

public:
    QColor glowColor() const { return m_glowColor; }
    void setGlowColor(QColor c) { m_glowColor = c; emit glowColorChanged(); update(); }
    float glowRadius() const { return m_glowRadius; }
    void setGlowRadius(float r) { m_glowRadius = r; emit glowRadiusChanged(); update(); }

signals:
    void glowColorChanged();
    void glowRadiusChanged();

protected:
    QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
        auto* node = static_cast<QSGGeometryNode*>(oldNode);
        auto* mat = node ? static_cast<GlowMaterial*>(node->material()) : nullptr;

        if (!node) {
            node = new QSGGeometryNode();

            auto* geom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4);
            geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
            updateGeometry(geom);    // 更新矩形顶点
            node->setGeometry(geom);
            node->setFlag(QSGNode::OwnsGeometry);

            mat = new GlowMaterial();
            node->setMaterial(mat);
            node->setFlag(QSGNode::OwnsMaterial);
        }

        // 更新材质 uniform
        mat->setGlowColor(m_glowColor);
        mat->setGlowRadius(m_glowRadius);
        node->markDirty(QSGNode::DirtyMaterial);

        return node;
    }

private:
    QColor m_glowColor = Qt::white;
    float m_glowRadius = 1.0f;
};

案例知识融合:本案例把自定义材质的完整链路串起来——①QSGMaterial 子类持有 uniform 数据;②QSGMaterialShader 负责将 uniform 打包到 GPU buffer;③compare() 决定批处理能否合并同类材质;④DirtyMaterial 标记使 Scene Graph 在下一帧调用 updateUniformData。

# 11.4.5 批处理断点

写了自定义 Shader 后,最容易被忽略的是批处理断点——Scene Graph 在 Render 阶段按前序遍历收集所有 QSGGeometryNode,对相邻的同材质节点合并为一个 Draw Call。以下任何差异都会打断合并:

Scene Graph 批处理收集器:
  for (node : QSGNode树(前序)):
    if (node是QSGGeometryNode):
      if (node.material.type() == currentBatch.material.type()
            && node.material.compare(currentBatch.material) == 0     ← 同材质?
            && node.geometry.attributes == currentBatch.attributes   ← 同顶点格式?
            && node.blendState == currentBatch.blendState):           ← 同混合模式?
        → 追加到当前 batch
      else:
        → flush 当前 batch → glDrawArrays
        → 开启新 batch,以当前 node 为起点

6 种断点来源及避免策略:

断点来源 触发条件 避免策略
材质类型不同 material.type() != prev.type() 同类节点放一起——不要穿插不同材质
compare() 不相等 uniform 值不同(如不同颜色) 逐顶点颜色(ColoredPoint2D)代替多个 material
顶点属性集不同 Point2D vs TexturedPoint2D 同一 batch 用同一种 attribute set
混合状态不同 有 Blending vs 无 分组:不透明节点在前、半透明在后
纹理不同 textureId != prev.textureId Texture Atlas 合并小图
编译或变换 QSGOpacityNode / QSGTransformNode 包裹 用 QSGMaterial 的 opacity 字段代替额外节点

探索性诊断——"我写了自定义 Shader 后 Draw Call 反而从 5 涨到 50"

现象: QML声明式 Rectangle x 50 = 5 Draw Call(QSG批处理合并)
       换成自定义 GlowRectangle x 50 = 50 Draw Call(全部无法合批)
  ↓
排查: 检查 compare() 实现:
  int compare(const QSGMaterial* other) const override {
      return int(m_glowColor.rgba64() - otherColor.rgba64());
  }
  → 第 1 个和第 2 个颜色相同,但第 2 个和第 3 个不同 → 连续合批被拆散

  → 修复: 逐顶点颜色 + 只读 uniform(glowRadius 同值可合并)
          → compare() 先用 glowRadius 分组,不比较逐顶点颜色
          → 50 个 Rectangle 变回 1 个 Draw Call(如果 glowRadius 全相同)

# 11.4.6 内置材质选用决策

在"够用"的前提下,优先用内置——省着色器编译、省 uniform 开销:

内置材质类 适用场景 Draw Call 影响
QSGFlatColorMaterial 纯色矩形/文字 ✅ 极简——单 uniform(color)
QSGVertexColorMaterial 逐顶点颜色(渐变色/粒子) ✅ 同批处理——无 per-material uniform
QSGTextureMaterial 纹理贴图 ⚠️ 不同纹理 = 不同 batch
QSGOpaqueTextureMaterial 不透明纹理(无 alpha) ✅ 比 TextureMaterial 少一次 alpha test

# 11.5 零拷贝纹理

# 11.5.1 原生纹理

// GPU 纹理句柄已经在显存中——不需要 CPU memcpy
GLuint nativeTexId = /* 来自摄像头 DMA / EGL Image / 外部 GL 上下文 */;

QQuickWindow* win = window();  // ← 只能在 Render Thread 中获取
auto* texture = win->createTextureFromNativeObject(
    QQuickWindow::NativeObjectTexture,
    &nativeTexId,                    // ← GLuint 纹理句柄
    0,                               // 额外的 native 选项
    QSize(1920, 1080),               // 纹理尺寸
    QQuickWindow::TextureHasAlphaChannel   // 是否有 alpha
);

底层等价操作(Scene Graph 不拷贝纹理数据):

Window::createTextureFromNativeObject(GLuint texId):
  ① 创建 QSGPlainTexture 包装对象
  ② 设置 m_textureId = texId(直接引用外部纹理)
  ③ 设置 m_ownsTexture = false(析构时不 glDeleteTextures)

Scene Graph 渲染时:
  glBindTexture(GL_TEXTURE_2D, texId)  ← 直接绑定 GPU 中的纹理
  glDrawArrays(...)                      ← 零拷贝绘制

# 11.5.2 摄像头链路

class CameraTextureItem : public QQuickItem {
    Q_OBJECT

public slots:
    // 从摄像头回调线程(或任意线程)接收新帧
    void onNewFrame(GLuint texId, int width, int height) {
        QMutexLocker lock(&m_mutex);
        m_newTexId  = texId;
        m_newWidth  = width;
        m_newHeight = height;
        m_hasNewFrame = true;
        lock.unlock();
        update();    // ← 必须在 GUI 线程
    }

protected:
    QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
        auto* node = static_cast<QSGGeometryNode*>(oldNode);

        // 检查新帧
        QMutexLocker lock(&m_mutex);
        bool hasFrame = m_hasNewFrame;
        GLuint texId = m_newTexId;
        lock.unlock();

        if (!node) {
            node = new QSGGeometryNode();

            // 创建全屏矩形几何——用于贴纹理
            auto* geom = new QSGGeometry(
                QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
            geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
            node->setGeometry(geom);
            node->setFlag(QSGNode::OwnsGeometry);

            auto* mat = new QSGTextureMaterial();
            node->setMaterial(mat);
            node->setFlag(QSGNode::OwnsMaterial);
        }

        // 更新几何(全屏矩形)
        auto* geom = node->geometry();
        auto* verts = geom->vertexDataAsTexturedPoint2D();
        float w = width(), h = height();
        verts[0].set(0, 0, 0, 0);
        verts[1].set(0, h, 0, 1);
        verts[2].set(w, 0, 1, 0);
        verts[3].set(w, h, 1, 1);

        // 新帧到来——创建新的纹理包装
        if (hasFrame) {
            auto* mat = static_cast<QSGTextureMaterial*>(node->material());
            auto* oldTex = mat->texture();

            auto* newTex = window()->createTextureFromNativeObject(
                QQuickWindow::NativeObjectTexture, &texId, 0,
                QSize(m_newWidth, m_newHeight));

            mat->setTexture(newTex);
            if (oldTex) delete oldTex;     // 释放旧纹理包装

            m_hasNewFrame = false;
        }

        node->markDirty(QSGNode::DirtyGeometry | QSGNode::DirtyMaterial);
        return node;
    }

private:
    QMutex m_mutex;
    GLuint m_newTexId = 0;
    int m_newWidth = 0, m_newHeight = 0;
    bool m_hasNewFrame = false;
};

# 11.5.3 摄像头预览

// 完整集成——V4L2 摄像头 → DMA buffer → EGL Image → QML Item
class V4L2CameraItem : public QQuickItem {
    Q_OBJECT

public:
    void startCapture() {
        // ① 打开 V4L2 设备
        m_fd = open("/dev/video0", O_RDWR);
        // ... 配置 format、请求 buffer

        // ② 启动捕获线程
        m_running = true;
        m_thread = std::thread([this]() {
            while (m_running) {
                // ③ 出队 buffer → DMA buffer fd
                v4l2_buffer buf = {};
                ioctl(m_fd, VIDIOC_DQBUF, &buf);

                // ④ DMA buffer → EGL Image → GL texture
                EGLint attrs[] = { EGL_DMA_BUF_PLANE0_FD_EXT, buf.m.fd, EGL_NONE };
                EGLImageKHR eglImage = eglCreateImageKHR(
                    eglGetCurrentDisplay(), EGL_NO_CONTEXT,
                    EGL_LINUX_DMA_BUF_EXT, nullptr, attrs);

                GLuint texId;
                glGenTextures(1, &texId);
                glBindTexture(GL_TEXTURE_2D, texId);
                glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage);

                // ⑤ 通知 QML——通过信号(Qt::AutoConnection 保证线程安全)
                emit newFrameReady(texId, m_width, m_height);

                // ⑥ 入队 buffer 回驱动
                ioctl(m_fd, VIDIOC_QBUF, &buf);
            }
        });
    }

signals:
    void newFrameReady(GLuint texId, int width, int height);

private:
    int m_fd = -1;
    std::thread m_thread;
    std::atomic<bool> m_running{false};
    int m_width = 1920, m_height = 1080;
};
V4L2CameraItem {
    id: camera
    anchors.fill: parent
    Component.onCompleted: camera.startCapture()
}

案例知识融合:本案例打通了"V4L2 DMA buffer → EGL Image → GL texture → QML Item"的完整零拷贝链路——全程 GPU 端到端,ARM Cortex-A53 上 30fps 稳定、CPU 占用 < 8%。


# 11.6 粒子系统

class ParticleSystem : public QQuickItem {
    Q_OBJECT
    static constexpr int MAX_PARTICLES = 5000;

    struct Particle { float x, y, vx, vy, life; };

    QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
        auto* node = static_cast<QSGGeometryNode*>(oldNode);
        if (!node) {
            node = new QSGGeometryNode();
            auto* geom = new QSGGeometry(
                QSGGeometry::defaultAttributes_ColoredPoint2D(), MAX_PARTICLES * 4);
            geom->setDrawingMode(QSGGeometry::DrawTriangles);
            node->setGeometry(geom);
            node->setFlag(QSGNode::OwnsGeometry);
            auto* mat = new QSGVertexColorMaterial();   // 内置逐顶点颜色材质
            node->setMaterial(mat);
            node->setFlag(QSGNode::OwnsMaterial);
        }
        auto* geom = node->geometry();
        auto* verts = geom->vertexDataAsColoredPoint2D();
        for (int i = 0; i < m_particleCount; ++i) {
            auto& p = m_particles[i];
            float s = 4.0f * (p.life / 2.0f);
            float a = p.life / 2.0f;
            // 每个粒子 = 2 个三角形(4 个顶点)
            verts[i*4+0].set(p.x-s, p.y-s, 1,1,1,a);
            verts[i*4+1].set(p.x+s, p.y-s, 1,1,1,a);
            verts[i*4+2].set(p.x-s, p.y+s, 1,1,1,a);
            verts[i*4+3].set(p.x+s, p.y+s, 1,1,1,a);
        }
        geom->allocate(m_particleCount * 4);
        node->markDirty(QSGNode::DirtyGeometry);
        return node;
    }

    void tick(float dt) {
        for (int i = 0; i < m_particleCount; ++i) {
            m_particles[i].x += m_particles[i].vx * dt;
            m_particles[i].y += m_particles[i].vy * dt;
            m_particles[i].life -= dt;
        }
        update();
    }

    Particle m_particles[MAX_PARTICLES];
    int m_particleCount = 0;
};

5000 个粒子 × 4 顶点 × 24 bytes = 480 KB 顶点缓冲——一次 Draw Call,60fps 稳定。

跨章知识串连——粒子系统看似只用了本章内容,实则串起了全书 5+ 章:

代码点 背后原理 参考章节
QSGGeometryNode 单节点 + 5000 粒子 批处理原理:同材质单节点→单 Draw Call §2.3.3 批处理
defaultAttributes_ColoredPoint2D 逐顶点颜色 材质共享——避免每粒子一个 material 破坏批处理 §11.4.3 uniform
tick(dt) + update() 双阶段 GUI 线程 tick → Render Thread updatePaintNode §2.4 双线程
markDirty(DirtyGeometry) 脏标记链路——只在数据变时上传 GPU §11.7 脏标
QSGGeometry 顶点数组直写 零拷贝路径——CPU 直接写 GPU 缓冲映射 §9.5.1 零拷贝
MAX_PARTICLES 常量池预分配 避免运行时 new/delete 抖动 GC §14.4 内存优化

探索性思考——为什么粒子不用 QML 的 Repeater { Rectangle } 实现?

试想用 Repeater 生成 5000 个 Rectangle 会怎样:

  1. 对象数量爆炸:5000 个 QQuickItem + 5000 个 QSGRectangleNode → JS 堆 + C++ 堆双重压力
  2. 绑定压力:每个 Rectangle 的 x/y/opacity 都是绑定 → 5000 × 4 = 20000 次 evaluate/帧
  3. Draw Call 灾难:即使同色,QSGRectangleNode 的圆角/边框状态可能打断批处理 → 100+ Draw Call
  4. GC 抖动:粒子生灭 → Repeater 增删 → QQuickItem 频繁 new/delete → 掉帧毛刺

结论倒推:QML 声明式适合"元素少、状态多"的场景;粒子系统是"元素多、状态少"——用 QSGNode 自定义反而更简单。这就是"什么时候不用 QML 声明式"的判断标准。


# 11.7 线程与脏标

操作 线程 注意事项
QQuickItem::update() GUI 线程 后台线程通过 emit signal + Qt::AutoConnection 桥接
updatePaintNode() Render Thread Sync 阶段 这里创建/访问 QSGNode 是安全的
QSGNode::markDirty() Render Thread 在 updatePaintNode 内调用
window()->createTextureFromNativeObject() Render Thread 必须在 updatePaintNode 内调用
Q_PROPERTY setter GUI 线程 setter 内 emit NOTIFY + update()
// ✅ 正确:后台线程通过信号桥接到 GUI 线程
// 传感器线程
void Sensor::onDataReady(const QVector<float>& data) {
    emit newData(data);    // Qt::AutoConnection → GUI 线程排队
}

// GUI 线程(connect 时自动)
connect(sensor, &Sensor::newData, qmlItem, [=](auto& d) {
    item->setProperty("values", QVariant::fromValue(d));  // GUI 线程安全
});

# 11.8 新手陷阱

# 陷阱 说明 修复
1 每帧 new QSGGeometryNode() 频繁分配释放 → 内存碎片 复用 oldNode,只更新顶点数据
2 忘记 setFlag(OwnsGeometry) oldNode 被 Scene Graph 回收时 geometry 泄漏 设置 OwnsGeometry / OwnsMaterial
3 window() 在 GUI 线程调用 window() 仅在 Render Thread 有效 只在 updatePaintNode 内使用 window()
4 后台线程直接 update() QQuickItem::update() 必须在 GUI 线程 信号 Qt::AutoConnection 桥接
5 compare() 没覆盖全部字段 不同参数被误判为相同 → 批处理错误 compare() 里逐字段比较

# 11.9 训练题

将下面的 Rectangle 动画改为自定义 QSGNode 实现

Rectangle {
    width: 200; height: 200
    color: "steelblue"
    NumberAnimation on scale { from: 0.5; to: 1.5; duration: 1000; loops: Animation.Infinite }
}

要求用 QSGGeometryNode + QSGTransformNode 实现同样的缩放动画。

参考答案
class PulsingRect : public QQuickItem {
    Q_OBJECT
    QSGNode* updatePaintNode(QSGNode* old, UpdatePaintNodeData*) override {
        auto* xform = static_cast<QSGTransformNode*>(old);
        if (!xform) {
            xform = new QSGTransformNode();
            auto* geomNode = new QSGGeometryNode();
            auto* geom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4);
            geom->setDrawingMode(QSGGeometry::DrawTriangleStrip);
            node->setGeometry(geom);
            // ... 设置材质、顶点
            xform->appendChildNode(geomNode);
        }
        QMatrix4x4 m;
        m.scale(scale());
        xform->setMatrix(m);
        return xform;
    }
};

# 11.10 速查表

概念 一句话
QSGNode 渲染树节点基类——Transform/Opacity/Clip/Geometry
QSGGeometryNode 核心绘制节点——顶点缓冲 + 材质
QSGGeometry 顶点缓冲——Point2D/ColoredPoint2D/TexturedPoint2D
QSGMaterial 材质抽象——着色器 + uniform + 纹理
QSGMaterialShader 着色器封装——updateUniformData + setShaderFiles
compare() 批处理合并判断——同材质 = 合批
OwnsGeometry 节点析构时释放 geometry
markDirty 标记需要更新——DirtyGeometry/DirtyMaterial/DirtyMatrix
createTextureFromNativeObject 外部 GL 纹理零拷贝包装
QSGVertexColorMaterial 内置逐顶点颜色材质——免写着色器
QSGFlatColorMaterial 内置纯色材质——Rectangle 的底层

核心哲学:

QSGNode = QML 渲染的性能天花板。

Canvas 是 JS 画完贴纹理,
QQuickItem+QSGNode 是直接写 GPU 顶点缓冲。

两者之间隔的不是代码量——是 FBO 到零拷贝的距离。

下一篇:12. 交叉编译与部署 (opens new window)

上次更新: 2026/07/12, 19:39:51
QML与C++集成原理
交叉编译与部署

← QML与C++集成原理 交叉编译与部署→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式