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 性能三问
- 像素数:分辨率越高越贵,全屏 Shader 慎重;
- 每帧都跑:动画期间每帧全量计算,静止时不要再重绘;
- 多 Shader 叠加:多个全屏特效叠加 = 多次逐像素计算。
4.2 渲染后端差异
- Skia:Shader 走 OpenGL,.frag 按 GLSL 编写;
- Impeller:新一代渲染引擎(Flutter 3.10+ 默认 iOS),对自定义 Shader 的支持 API 在演进,升级前要在目标平台真机验证;
- Web:走 WebGL,语法与能力有差异,不是所有 .frag 都能直接跑。
五、项目建议
能源 App 的图表与看板若要做视觉增强:
- 优先用动画 +
CustomPainter+Gradient达到效果; - 确需 Shader 时,用局部小区域(图表水波、背景块),不做全屏常驻;
- Shader 资源统一放
assets/shaders/,命名与平台无关; - 真机验证 Skia/Impeller/Web 三端,避免只在模拟器通过。
动手练习
- 写一个 30 行的水波 .frag,用
FragmentProgram渲染到CustomPainter; - 用
AnimationController驱动 time,让水波动起来; - 在真机与模拟器各跑一次,记录帧率与功耗差异。
自检清单
- [ ] 能说清 Fragment Shader 是"逐像素计算颜色"
- [ ] 会加载 .frag 资源并用
setFloat注入 uniform - [ ] 知道性能三问(像素数 / 每帧 / 叠加)
- [ ] 知道 Skia / Impeller / Web 的 Shader 差异
- [ ] 能在"普通动画够不够"上做克制决策
常见误区
- "Shader 万能":普通渐变、阴影、圆角用框架能力更划算;
- "静态画面也用 Shader":不动的画面没必要每帧计算,用静态绘制;
- "模拟器过了就行":渲染后端差异大,必须真机 + 平台验证;
- "全屏特效很酷就上":长驻全屏 Shader 的功耗会让用户卸载。
相关文档
- 00 · 学习路线图 —— 阅读顺序
- 28 · 自定义渲染实战 —— CustomPainter 与绘制基础
- 40 · 图表与数据可视化 —— Shader 特效的落地场景
- 29 · 动画进阶与动效工程化 —— 动画驱动的配合
- 22 · 性能分析与优化实战 —— 帧率与功耗验证