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

