跳转至正文

编写和使用片段着色器

如何编写和使用片段着色器,在你的 Flutter 应用中创建自定义视觉效果。

自定义着色器可用于提供超出 Flutter SDK 所提供的丰富图形效果。着色器是用一种类似 Dart 的小型语言(称为 GLSL)编写、并在用户 GPU 上执行的程序。

通过在 pubspec.yaml 文件中列出自定义着色器可将其添加到 Flutter 项目,并使用 FragmentProgram API 获取。

向应用添加着色器

#

着色器以带 .frag 扩展名的 GLSL 文件形式,必须在项目 pubspec.yaml 文件的 shaders 部分声明。Flutter 命令行工具会将着色器编译为相应的后端格式,并生成所需的运行时元数据。编译后的着色器会像资源一样包含在应用中。

yaml
flutter:
  shaders:
    - shaders/myshader.frag

在 debug 模式下运行时,对着色器程序的更改会触发重新编译,并在热重载或热重启期间更新着色器。

来自包的着色器通过在给色器程序名称前加上 packages/$pkgname 前缀添加到项目中(其中 $pkgname 是包名)。

在运行时加载着色器

#

要在运行时将着色器加载到 FragmentProgram 对象,请使用 FragmentProgram.fromAsset 构造函数。资源名称与 pubspec.yaml 文件中给出的着色器路径相同。

dart
void loadMyShader() async {
  var program = await FragmentProgram.fromAsset('shaders/myshader.frag');
}

FragmentProgram 对象可用于创建一个或多个 FragmentShader 实例。FragmentShader 对象表示片段程序以及一组特定的 uniforms(配置参数)。可用的 uniforms 取决于着色器的定义方式。

dart
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)会忽略着色器的值。

dart
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

dart
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,
        ),
      ),
    ),
  );
}

ImageFilterBackdropFilter 一起使用时,可用 ClipRect 限制受 ImageFilter 影响的区域。没有 ClipRect 时,BackdropFilter 将应用于整个屏幕。

ImageFilter 片段着色器会从引擎自动接收一些 uniforms。索引 0 处的 sampler2D 值设为滤镜输入图像,索引 0 和 1 处的 float 值设为图像的宽度和高度。你的着色器必须指定此构造函数以接受这些值(例如 sampler2Dvec2),但你不应从 Dart 代码中设置它们。

针对 OpenGLES 时,纹理的 y 坐标会翻转,因此片段着色器在从引擎提供的纹理采样时应取消翻转 UV。

glsl
#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 类型为 floatvec2vec3vec4 的浮点 uniform 使用 FragmentShader.setFloatFragmentShader.getUniformFloat 方法设置。使用 sampler2D 类型的 GLSL 采样器值使用 FragmentShader.setImageSamplerFragmentShader.getImageSampler 方法设置。

每个 uniform 值的正确索引由片段程序中 uniform 值的定义顺序决定。对于由多个 float 组成的数据类型(如 vec4),你必须为每个值调用一次 FragmentShader.setFloatUniformFloatSlot.set

例如,给定 GLSL 片段程序中的以下 uniform 声明:

glsl
uniform float uScale;
uniform sampler2D uTexture;
uniform vec2 uMagnitude;
uniform vec4 uColor;

初始化这些 uniform 值的对应 Dart 代码如下:

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 偏移等信息。

shell
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 函数访问。例如:

glsl
#include <flutter/runtime_effect.glsl>

void main() {
  vec2 currentPos = FlutterFragCoord().xy;
}

FlutterFragCoord 返回的值与 gl_FragCoord 不同。gl_FragCoord 提供屏幕空间坐标,通常应避免使用,以确保着色器在各后端间一致。针对 Skia 后端时,对 gl_FragCoord 的调用会重写为访问局部坐标,但 Impeller 无法进行此重写。

颜色

#

没有用于颜色的内置数据类型。通常用 vec4 表示,每个分量对应 RGBA 颜色通道之一。

单一输出 fragColor 要求颜色值归一化到 0.01.0 范围,并具有预乘 alpha。这与典型的 Flutter 颜色不同,后者使用 0-255 值编码且具有非预乘 alpha。

采样器

#

采样器提供对 dart:ui Image 对象的访问。该图像可从解码后的图像获取,或使用 Scene.toImageSyncPicture.toImageSync 从应用的一部分获取。

GLSL 中的采样器用法示例
#
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 示例
#
dart
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

其他资源

#

更多信息请参阅以下资源。