Skip to content

41 · 自定义 Shader 与 GLSL ​

一句话定位:Shader 是"在 GPU 上逐像素算颜色"的代码,对应前端的 WebGL/Canvas 滤镜与 CSS 特效。Flutter 里用 FragmentShader 跑 GLSL,适合做水波、噪点、渐变、扭曲这类视觉增强,但它是压轴特效,不是日常工具。

开篇速览 ​

问题答案
前端对应物WebGL Shader、CSS filter、Canvas 渐变
核心概念FragmentShader + GLSL .frag 文件 + ShaderBuilder 变量注入
最适合图表水波填充、背景渐变动画、页面转场特效
一句话红线Shader 让每帧的每个像素都跑一次计算,滥用 = 性能灾难;先用动画/CustomPainter,够用就别上 Shader

代码来源说明:HaierEnergy 当前 lib/ 未使用 FragmentShader(特效走普通动画与 CustomPainter)。 本篇为能力篇,给出原理与接入姿势。


一、Shader 是什么 ​

普通绘制是 CPU 决定"画什么形状、什么颜色";Shader 是把"每个像素的颜色"交给 GPU 上的小程序逐像素计算:

glsl
// .frag 文件:最简单的高斯模糊/渐变示例(伪代码)
uniform vec2 resolution;
uniform float time;

void main() {
  vec2 uv = gl_FragCoord.xy / resolution;
  float wave = 0.5 + 0.5 * sin(uv.x * 20.0 + time);
  gl_FragColor = vec4(uv.x, wave, 1.0 - uv.x, 1.0);
}

Shader 的两个典型入口:

类型作用
Vertex Shader决定顶点位置(几何变换)
Fragment Shader决定每个像素颜色(Flutter 最常用)

二、Flutter 怎么用 FragmentShader ​

2.1 加载 .frag 资源 ​

dart
// 1. 把 .frag 放进 assets,注册到 pubspec.yaml
// 2. 运行时编译 + 创建 shader
final source = await rootBundle.loadString('assets/shaders/wave.frag');
final shader = await FragmentProgram.fromAsset('assets/shaders/wave.frag');
final fragment = shader.fragmentShader();

2.2 注入 uniform 并绘制 ​

dart
// 在 CustomPainter 里用 shader 绘制
fragment
  ..setFloat(0, size.width)   // resolution.x
  ..setFloat(1, size.height)  // resolution.y
  ..setFloat(2, _time);       // time

final paint = Paint()..shader = fragment;
canvas.drawRect(Offset.zero & size, paint);

2.3 动画驱动 ​

用 AnimationController 每帧更新 time,并在 shouldRepaint 返回 true 触发重绘:

dart
class _WavePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    shader.setFloat(2, controller.value * 10);
    canvas.drawRect(Offset.zero & size, Paint()..shader = shader);
  }

  @override
  bool shouldRepaint(covariant _WavePainter old) => true; // 动画期间每帧重绘
}

三、适合与不适合 ​

场景是否推荐原因
图表水波填充/扫描线✅特效对数据展示有加分,性能可控
品牌启动动画✅一次性动画,成本可接受
页面转场/背景微动效✅克制使用,注意功耗
全屏常驻模糊/粒子⚠️每像素计算,长驻功耗高
替代普通渐变/阴影❌CustomPainter/Decoration 就够了

四、性能与兼容 ​

4.1 性能三问 ​

  1. 像素数:分辨率越高越贵,全屏 Shader 慎重;
  2. 每帧都跑:动画期间每帧全量计算,静止时不要再重绘;
  3. 多 Shader 叠加:多个全屏特效叠加 = 多次逐像素计算。

4.2 渲染后端差异 ​

  • Skia:Shader 走 OpenGL,.frag 按 GLSL 编写;
  • Impeller:新一代渲染引擎(Flutter 3.10+ 默认 iOS),对自定义 Shader 的支持 API 在演进,升级前要在目标平台真机验证;
  • Web:走 WebGL,语法与能力有差异,不是所有 .frag 都能直接跑。

五、项目建议 ​

能源 App 的图表与看板若要做视觉增强:

  1. 优先用动画 + CustomPainter + Gradient 达到效果;
  2. 确需 Shader 时,用局部小区域(图表水波、背景块),不做全屏常驻;
  3. Shader 资源统一放 assets/shaders/,命名与平台无关;
  4. 真机验证 Skia/Impeller/Web 三端,避免只在模拟器通过。

动手练习 ​

  1. 写一个 30 行的水波 .frag,用 FragmentProgram 渲染到 CustomPainter;
  2. 用 AnimationController 驱动 time,让水波动起来;
  3. 在真机与模拟器各跑一次,记录帧率与功耗差异。

自检清单 ​

  • [ ] 能说清 Fragment Shader 是"逐像素计算颜色"
  • [ ] 会加载 .frag 资源并用 setFloat 注入 uniform
  • [ ] 知道性能三问(像素数 / 每帧 / 叠加)
  • [ ] 知道 Skia / Impeller / Web 的 Shader 差异
  • [ ] 能在"普通动画够不够"上做克制决策

常见误区 ​

  • "Shader 万能":普通渐变、阴影、圆角用框架能力更划算;
  • "静态画面也用 Shader":不动的画面没必要每帧计算,用静态绘制;
  • "模拟器过了就行":渲染后端差异大,必须真机 + 平台验证;
  • "全屏特效很酷就上":长驻全屏 Shader 的功耗会让用户卸载。

相关文档 ​