编写和使用片段着色器
如何编写和使用片段着色器,以在您的 Flutter 应用程序中创建自定义视觉效果。
自定义着色器可用于提供超出 Flutter SDK 提供的丰富图形效果。着色器是用一种小型、类似 Dart 的语言(称为 GLSL)编写的程序,并在用户的 GPU 上执行。
自定义着色器通过在 pubspec.yaml 文件中列出它们,并使用 FragmentProgram API 获取,添加到 Flutter 项目中。
将着色器添加到应用程序
#着色器以具有 .frag 扩展名的 GLSL 文件形式存在,必须在项目 pubspec.yaml 文件的 shaders 部分中声明。Flutter 命令行工具会将着色器编译为其适当的后端格式,并生成其必要运行时元数据。然后,编译后的着色器就像资产一样包含在应用程序中。
flutter:
shaders:
- shaders/myshader.frag
在调试模式下运行时,对着色器程序的更改会触发重新编译并在热重载或热重启期间更新着色器。
来自包的着色器通过在着色器程序名称前加上 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
#片段着色器可以通过设置 Paint.shader 与大多数 Canvas API 一起使用。例如,在使用 Canvas.drawRect 时,着色器会为矩形内的所有片段进行评估。对于带有描边路径的 API(如 Canvas.drawPath),着色器会为描边线内的所有片段进行评估。某些 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 一起使用。这允许使用自定义片段着色器与 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 扩展名。)
支持从 460 到 100 的任何 GLSL 版本,尽管某些可用功能受到限制。本文档其余示例使用版本 460 core。
在 Flutter 中使用时,着色器受到以下限制
- 不支持 UBO 和 SSBO
sampler2D是唯一支持的采样器类型- 仅支持
texture的两个参数版本(采样器和 uv) - 不能声明额外的 varying 输入
- 在定位 Skia 时,所有精度提示都会被忽略
- 不支持无符号整数和布尔值
Uniforms
#可以通过在 GLSL 着色器源代码中定义 uniform 值,然后在每个片段着色器实例的 Dart 中设置这些值来配置片段程序。
使用 GLSL 类型 float、vec2、vec3 和 vec4 的浮点 uniforms 使用 FragmentShader.setFloat 或 FragmentShader.getUniformFloat 方法设置。使用 GLSL 采样器值(使用 sampler2D 类型)使用 FragmentShader.setImageSampler 或 FragmentShader.getImageSampler 方法设置。
每个 uniform 值的正确索引由片段程序中 uniform 值定义的顺序确定。对于由多个浮点数组成的的数据类型(例如 vec4),您必须为每个值调用 FragmentShader.setFloat 或 UniformFloatSlot.set 一次。
例如,给定片段程序中以下 uniforms 声明
uniform float uScale;
uniform sampler2D uTexture;
uniform vec2 uMagnitude;
uniform vec4 uColor;
相应的 Dart 代码来初始化这些 uniform 值如下
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 重新开始。
任何未初始化的浮点 uniforms 默认值为 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 上的 编写高效着色器。
其他资源
#有关更多信息,这里有一些资源。
- The Book of Shaders by Patricio Gonzalez Vivo and Jen Lowe
- Shader toy,一个协作着色器游乐场
-
simple_shader,一个简单的 Flutter 片段着色器示例项目 -
flutter_shaders,一个简化在 Flutter 中使用片段着色器的包