编写和使用片段着色器
如何编写和使用片段着色器,在你的 Flutter 应用中创建自定义视觉效果。
自定义着色器可用于提供超出 Flutter SDK 所提供的丰富图形效果。着色器是用一种类似 Dart 的小型语言(称为 GLSL)编写、并在用户 GPU 上执行的程序。
通过在 pubspec.yaml 文件中列出自定义着色器可将其添加到 Flutter 项目,并使用 FragmentProgram
API 获取。
向应用添加着色器
#
着色器以带 .frag 扩展名的 GLSL 文件形式,必须在项目 pubspec.yaml 文件的 shaders 部分声明。Flutter 命令行工具会将着色器编译为相应的后端格式,并生成所需的运行时元数据。编译后的着色器会像资源一样包含在应用中。
flutter:
shaders:
- shaders/myshader.frag
在 debug 模式下运行时,对着色器程序的更改会触发重新编译,并在热重载或热重启期间更新着色器。
来自包的着色器通过在给色器程序名称前加上 packages/$pkgname 前缀添加到项目中(其中 $pkgname 是包名)。
在运行时加载着色器
#
要在运行时将着色器加载到 FragmentProgram 对象,请使用 FragmentProgram.fromAsset
构造函数。资源名称与 pubspec.yaml 文件中给出的着色器路径相同。
void loadMyShader() async {
var program = await FragmentProgram.fromAsset('shaders/myshader.frag');
}
FragmentProgram 对象可用于创建一个或多个 FragmentShader
实例。FragmentShader 对象表示片段程序以及一组特定的 uniforms(配置参数)。可用的 uniforms 取决于着色器的定义方式。
void updateShader(Canvas canvas, Rect rect, FragmentProgram program) {
var shader = program.fragmentShader();
shader.setFloat(0, 42.0);
canvas.drawRect(rect, Paint()..shader = shader);
}
Canvas API
#Canvas API
#
片段着色器可通过设置 Paint.shader
与大多数 Canvas API 一起使用。例如,使用 Canvas.drawRect
时,着色器会对矩形内的所有片段求值。对于像 Canvas.drawPath
这样绘制描边路径的 API,着色器会对描边线内的所有片段求值。某些 API(如 Canvas.drawImage)会忽略着色器的值。
void paint(Canvas canvas, Size size, FragmentShader shader) {
// Draws a rectangle with the shader used as a color source.
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..shader = shader,
);
// Draws a stroked rectangle with the shader only applied to the fragments
// that lie within the stroke.
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()
..style = PaintingStyle.stroke
..shader = shader,
)
}
ImageFilter API
#ImageFilter API
#
片段着色器也可与 ImageFilter
API 一起使用。这允许将自定义片段着色器与 ImageFiltered
类或 BackdropFilter
类配合,将着色器应用于已渲染的内容。ImageFilter
提供构造函数 ImageFilter.shader,用于创建带自定义片段着色器的
ImageFilter。
Widget build(BuildContext context, FragmentShader shader) {
return ClipRect(
child: SizedBox(
width: 300,
height: 300,
child: BackdropFilter(
filter: ImageFilter.shader(shader),
child: Container(
color: Colors.transparent,
),
),
),
);
}
将 ImageFilter
与 BackdropFilter
一起使用时,可用 ClipRect
限制受 ImageFilter
影响的区域。没有 ClipRect
时,BackdropFilter
将应用于整个屏幕。
ImageFilter 片段着色器会从引擎自动接收一些 uniforms。索引 0 处的 sampler2D 值设为滤镜输入图像,索引 0 和 1 处的
float 值设为图像的宽度和高度。你的着色器必须指定此构造函数以接受这些值(例如 sampler2D 和 vec2),但你不应从 Dart 代码中设置它们。
针对 OpenGLES 时,纹理的 y 坐标会翻转,因此片段着色器在从引擎提供的纹理采样时应取消翻转 UV。
#version 460 core
#include <flutter/runtime_effect.glsl>
out vec4 fragColor;
// These uniforms are automatically set by the engine.
uniform vec2 u_size;
uniform sampler2D u_texture;
void main() {
vec2 uv = FlutterFragCoord().xy / u_size;
#ifdef IMPELLER_TARGET_OPENGLES
// When sampling from u_texture on OpenGLES the y-coordinates will be flipped.
uv.y = 1.0 - uv.y;
#endif
vec4 color = texture(u_texture, uv);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
fragColor = vec4(vec3(gray), color.a);
}
编写着色器
#
片段着色器以 GLSL 源文件编写。按惯例,这些文件使用 .frag 扩展名。(Flutter 不支持顶点着色器,顶点着色器会使用 .vert 扩展名。)
支持 GLSL 版本 460 至 100,但部分可用功能受限。本文档其余示例使用 460 core 版本。
在 Flutter 中使用着色器时受以下限制:
不支持 UBO 和 SSBO
-
sampler2D是唯一支持的采样器类型 -
仅支持
texture的两参数版本(采样器和 uv) 不能声明额外的 varying 输入
针对 Skia 时,所有精度提示都会被忽略
不支持无符号整数和布尔值
Uniforms
#Uniforms
#可通过在 GLSL 着色器源中定义 uniform 值,然后为每个片段着色器实例在 Dart 中设置这些值来配置片段程序。
GLSL 类型为 float、vec2、vec3 和 vec4 的浮点 uniform 使用 FragmentShader.setFloat
或 FragmentShader.getUniformFloat
方法设置。使用 sampler2D 类型的 GLSL 采样器值使用 FragmentShader.setImageSampler
或 FragmentShader.getImageSampler
方法设置。
每个 uniform 值的正确索引由片段程序中 uniform 值的定义顺序决定。对于由多个 float 组成的数据类型(如 vec4),你必须为每个值调用一次
FragmentShader.setFloat
或 UniformFloatSlot.set。
例如,给定 GLSL 片段程序中的以下 uniform 声明:
uniform float uScale;
uniform sampler2D uTexture;
uniform vec2 uMagnitude;
uniform vec4 uColor;
初始化这些 uniform 值的对应 Dart 代码如下:
class Foobar {
late final UniformFloatSlot _scale;
late final List<UniformFloatSlot> _magnitude;
late final List<UniformFloatSlot> _color;
late final ImageSamplerSlot _texture;
void setUp(FragmentShader shader) {
_scale = shader.getUniformFloat('uScale');
_magnitude = List<UniformFloatSlot>.generate(2, (int index) {
return shader.getUniformFloat('uMagnitude', index);
});
_color = List<UniformFloatSlot>.generate(4, (int index) {
return shader.getUniformFloat('uColor', index);
});
_texture = shader.getImageSampler('uTexture');
}
void update(Color color, Image image) {
_scale.set(23);
_magnitude[0].set(114);
_magnitude[1].set(83);
_color[0].set(color.r * color.a);
_color[1].set(color.g * color.a);
_color[2].set(color.b * color.a);
_color[3].set(color.a);
_texture.set(image);
}
}
使用 FragmentShader.setFloat
时注意,索引不计入 sampler2D uniform。该 uniform 使用 FragmentShader.setImageSampler
单独设置,索引从 0 重新开始。
任何未初始化的 float uniform 默认为 0.0。
可以使用以下命令审计 Flutter 着色器编译器生成的反射数据,以查看 uniform 偏移等信息。
cd $FLUTTER
# Generate the .sl file.
`find bin/ -name impellerc` \
--runtime-stage-metal \
--iplr \
--input=path/to/myshader.frag \
--sl=foo.sl \
--spirv=foo.spirv \
--include=engine/src/flutter/impeller/compiler/shader_lib/ \
--input-type=frag
# Convert the .sl file to .json
flatc \
--json \
./engine/src/flutter/impeller/runtime_stage/runtime_stage.fbs \
-- ./foo.sl
# View results
cat foo.json
当前位置
#
着色器可访问包含当前被求值片段的局部坐标的 varying 值。使用此功能可计算依赖当前位置的效果,可通过导入 flutter/runtime_effect.glsl
库并调用 FlutterFragCoord 函数访问。例如:
#include <flutter/runtime_effect.glsl>
void main() {
vec2 currentPos = FlutterFragCoord().xy;
}
FlutterFragCoord 返回的值与 gl_FragCoord 不同。gl_FragCoord 提供屏幕空间坐标,通常应避免使用,以确保着色器在各后端间一致。针对 Skia 后端时,对
gl_FragCoord 的调用会重写为访问局部坐标,但 Impeller 无法进行此重写。
颜色
#没有用于颜色的内置数据类型。通常用 vec4 表示,每个分量对应 RGBA 颜色通道之一。
单一输出 fragColor 要求颜色值归一化到 0.0 至 1.0 范围,并具有预乘 alpha。这与典型的 Flutter 颜色不同,后者使用
0-255 值编码且具有非预乘 alpha。
采样器
#
采样器提供对 dart:ui Image 对象的访问。该图像可从解码后的图像获取,或使用 Scene.toImageSync
或 Picture.toImageSync
从应用的一部分获取。
GLSL 中的采样器用法示例
##include <flutter/runtime_effect.glsl>
uniform vec2 uSize;
uniform sampler2D uTexture;
out vec4 fragColor;
void main() {
vec2 uv = FlutterFragCoord().xy / uSize;
fragColor = texture(uTexture, uv);
}
默认情况下,图像使用 TileMode.clamp
确定超出 [0, 1] 范围的值的行为。不支持自定义平铺模式,需要在着色器中模拟。
toImageSync 示例
#
class SDFPainter {
SDFPainter(this.sdfShader, this.renderShader);
FragmentShader sdfShader;
FragmentShader renderShader;
Image? _sdf;
bool isDirty = false;
double radius = 0.5;
void paint(Canvas canvas, Size size) {
if (_sdf == null || isDirty) {
final recorder = PictureRecorder();
final subCanvas = Canvas(recorder);
final paint = Paint()..shader = sdfShader;
sdfShader.setFloat(0, size.width);
sdfShader.setFloat(1, size.height);
sdfShader.setFloat(2, radius);
subCanvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
final picture = recorder.endRecording();
_sdf = picture.toImageSync(size.width.toInt(), size.height.toInt());
isDirty = false;
}
renderShader.setFloat(0, size.width);
renderShader.setFloat(1, size.height);
renderShader.setImageSampler(0, _sdf!);
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..shader = renderShader,
);
}
}
性能注意事项
#针对 Skia 后端时,加载着色器可能开销较大,因为必须在运行时将着色器编译为相应的平台特定着色器。如果你打算在动画期间使用一个或多个着色器,考虑在动画开始前预缓存片段程序对象。
你可以跨帧复用 FragmentShader 对象;这比每帧创建新的 FragmentShader 更高效。
有关编写高性能着色器的更详细指南,请查看 GitHub 上的 Writing efficient shaders。
其他资源
#更多信息请参阅以下资源。
-
Patricio Gonzalez Vivo 和 Jen Lowe 撰写的 The Book of Shaders
-
Shader toy,协作式着色器游乐场
-
simple_shader,简单的 Flutter 片段着色器示例项目 -
flutter_shaders,简化在 Flutter 中使用片段着色器的包
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-04。查看文档源码 或者 为本页面内容提出建议。