You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中结合canvas.drawVertices与自定义片段着色器批量渲染大量绳状线条的性能优化方案咨询

Flutter中结合canvas.drawVertices与自定义片段着色器批量渲染大量绳状线条的性能优化方案咨询

我完全懂你现在的痛点——2万多条线用canvas.drawRect循环渲染,每条都要单独设置一堆uniform参数,GPU上下文切换的开销直接拉满,不卡才怪!你想转用canvas.drawVertices批量渲染来救性能,但不知道怎么和自定义片段着色器结合,对吧?我来给你梳理下具体的实现思路和步骤,亲测能把性能提上去一大截。

为什么原来的方案性能拉胯?

  • 每次canvas.drawRect都是一次独立的GPU绘制调用,2万次调用意味着GPU要做2万次上下文切换、状态设置,这开销是指数级的。
  • 每条线单独传uniform,GPU每次都要重新读取这些参数,完全没有批量处理的优势。

核心优化思路:批量打包顶点数据 + 着色器适配

我们要把所有线条的渲染数据(顶点、线条起点/终点、颜色)一次性打包成Vertices对象,通过顶点属性传递给着色器,而不是每条线单独设置uniform。这样GPU只需要一次绘制调用就能处理所有线条,效率直接起飞。

具体实现步骤

第一步:重构自定义着色器(顶点+片段)

原来的着色器用uniform接收单条线的数据,现在要改成用顶点属性批量接收所有线条的数据,再通过顶点着色器传递给片段着色器。

顶点着色器修改

// 默认的顶点位置属性(对应Dart端的Offset列表)
attribute vec2 a_position;
// 每条线的起点(自定义顶点属性)
attribute vec2 a_lineStart;
// 每条线的终点(自定义顶点属性)
attribute vec2 a_lineEnd;
// 每条线的颜色(自定义顶点属性,normalized后直接是0-1的float)
attribute vec3 a_lineColor;

// 传递给片段着色器的变量
varying vec2 v_lineStart;
varying vec2 v_lineEnd;
varying vec3 v_lineColor;
varying vec2 v_fragPos;

void main() {
    // 输出顶点位置(如果需要适配Flutter的坐标空间,可能要加投影矩阵,这里用默认逻辑)
    gl_Position = vec4(a_position, 0.0, 1.0);
    // 把顶点属性传递给片段着色器
    v_lineStart = a_lineStart;
    v_lineEnd = a_lineEnd;
    v_lineColor = a_lineColor;
    // 传递片元在包围盒中的位置,用于原来的绳状纹理计算
    v_fragPos = a_position;
}

片段着色器修改

把原来依赖uniform的线条数据,全部换成顶点着色器传递来的varying变量:

// 原来的uniform float u_lineData0; → 替换成 v_lineStart.x
// 原来的uniform float u_lineData1; → 替换成 v_lineStart.y
// 原来的uniform float u_lineData2; → 替换成 v_lineEnd.x
// 原来的uniform float u_lineData3; → 替换成 v_lineEnd.y
// 原来的uniform float u_lineData4~u_lineData6; → 替换成 v_lineColor.r/g/b

// 保留你原来的绳状纹理渲染逻辑,只替换数据来源
void main() {
    // 用v_lineStart、v_lineEnd、v_lineColor代替原来的uniform参数
    // ... 你的原有绳状纹理计算代码 ...
    gl_FragColor = vec4(/* 计算后的颜色 */, 1.0);
}

第二步:Dart端批量构建Vertices对象

我们要把所有线条的包围盒拆成三角形顶点,每个顶点携带对应线条的专属数据,打包成Vertices对象:

Vertices _createVertices() {
  final List<Offset> positions = [];
  final List<double> lineStarts = []; // 每个顶点的线条起点(x,y)
  final List<double> lineEnds = [];   // 每个顶点的线条终点(x,y)
  final List<int> lineColors = [];    // 每个顶点的颜色(r,g,b),用byte节省内存

  for (final lineData in lines) {
    // 计算线条的包围盒(和你原来的逻辑一致,稍微扩大)
    final minX = math.min(lineData.startPoint.dx, lineData.endPoint.dx) - 15;
    final maxX = math.max(lineData.startPoint.dx, lineData.endPoint.dx) + 15;
    final minY = math.min(lineData.startPoint.dy, lineData.endPoint.dy) - 15;
    final maxY = math.max(lineData.startPoint.dy, lineData.endPoint.dy) + 15;

    // 包围盒的4个顶点
    final v0 = Offset(minX, minY);
    final v1 = Offset(maxX, minY);
    final v2 = Offset(maxX, maxY);
    final v3 = Offset(minX, maxY);

    // 把矩形拆成2个三角形(共6个顶点),WebGL只认三角形
    final triangle1 = [v0, v1, v2];
    final triangle2 = [v0, v2, v3];
    positions.addAll(triangle1);
    positions.addAll(triangle2);

    // 每条线的固定数据(6个顶点复用同一组数据)
    final startX = lineData.startPoint.dx;
    final startY = lineData.startPoint.dy;
    final endX = lineData.endPoint.dx;
    final endY = lineData.endPoint.dy;
    final r = lineData.color.red;
    final g = lineData.color.green;
    final b = lineData.color.blue;

    // 给6个顶点添加相同的线条数据
    for (int i = 0; i < 6; i++) {
      lineStarts.addAll([startX, startY]);
      lineEnds.addAll([endX, endY]);
      lineColors.addAll([r, g, b]);
    }
  }

  // 转成GPU能识别的格式
  final lineStartsList = Float32List.fromList(lineStarts);
  final lineEndsList = Float32List.fromList(lineEnds);
  final lineColorsList = Uint8List.fromList(lineColors);

  // 定义自定义顶点属性
  final vertexAttribs = [
    // 属性1:线条起点(vec2,2个float)
    VertexAttrib(1, VertexAttribType.float, 2),
    // 属性2:线条终点(vec2,2个float)
    VertexAttrib(2, VertexAttribType.float, 2),
    // 属性3:线条颜色(vec3,3个byte,normalized=true自动转0-1的float)
    VertexAttrib(3, VertexAttribType.ubyte, 3, normalized: true),
  ];

  // 创建三角形顶点对象
  return Vertices.triangles(
    positions,
    vertexAttribs: vertexAttribs,
    attribs: [
      lineStartsList,  // 对应属性1
      lineEndsList,    // 对应属性2
      lineColorsList,  // 对应属性3
    ],
  );
}

第三步:批量渲染调用

修改你的绘制逻辑,一次性提交所有顶点,不再循环设置uniform:

void _paintWithVertices(Canvas canvas, Size size) {
  // 缓存Vertices,只在数据变化时重新生成(和你原来的逻辑一致)
  if (_needsVerticesUpdate || _cachedVertices == null) {
    _needsVerticesUpdate = false;
    _cachedVertices = _createVertices();
  }

  canvas.save();

  // 应用你的SVG变换逻辑(和原来一致)
  if (originalSvgLines.isNotEmpty) {
    final canvasCenterX = canvasSize.width / 2;
    final canvasCenterY = canvasSize.height / 2;
    final svgCenterX = svgBounds.center.dx;
    final svgCenterY = svgBounds.center.dy;
    final transform = Matrix4.identity()
      ..translate(canvasCenterX + svgTranslateX, canvasCenterY + svgTranslateY)
      ..scale(svgScale)
      ..translate(-svgCenterX, -svgCenterY);
    canvas.transform(transform.storage);
  }

  // 配置自定义着色器(现在不需要循环设置线条数据的uniform了!)
  final paint = Paint()..shader = yourCustomShader; // 这里是你修改后的着色器

  // 一次性渲染所有顶点
  canvas.drawVertices(_cachedVertices!, BlendMode.srcOver, paint);

  canvas.restore();
}

额外性能优化小贴士

  1. 缓存Vertices对象:像你原来的代码那样,只在线条数据变化时重新生成,避免每次绘制都做大量计算。
  2. 压缩顶点数据:用ubyte存储颜色(如上面的代码),比float节省75%的内存,GPU处理也更快。
  3. 减少顶点数量:如果线条的包围盒可以复用(比如多条线的包围盒重叠),可以考虑合并,但一般2万条线的话,6*2万=12万顶点,GPU完全能处理。
  4. 调试着色器:如果渲染出问题,可以先把片段着色器简化成直接输出线条颜色,确认顶点数据传递正确后,再逐步加回绳状纹理的逻辑。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:23:05