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

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

向应用添加着色器

#

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

yaml
flutter:
  shaders:
    - shaders/myshader.frag

在调试模式下运行应用时,着色器程序的变化会触发重新编译,并在热重载或热重启期间更新着色器。

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

运行时加载着色器

#

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

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

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

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

#

通过设置 `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,
  )
}

创作着色器

#

片段着色器以 GLSL 源文件形式编写。按照惯例,这些文件的扩展名为 `.frag`。(Flutter 不支持顶点着色器,其扩展名为 `.vert`。)

支持从 460 到 100 的任何 GLSL 版本,但某些可用功能受到限制。本文档中的其余示例使用 `460 core` 版本。

与 Flutter 一起使用时,着色器受到以下限制:

  • 不支持 UBO 和 SSBO
  • `sampler2D` 是唯一支持的采样器类型
  • 只支持 `texture` 的双参数版本(采样器和 uv)
  • 不能声明额外的 varying 输入
  • 当目标是 Skia 时,所有精度提示都会被忽略
  • 不支持无符号整数和布尔值

统一变量

#

可以通过在 GLSL 着色器源中定义 `uniform` 值,然后为每个片段着色器实例在 Dart 中设置这些值来配置片段程序。

使用 `FragmentShader.setFloat` 方法设置 GLSL 类型为 `float`、`vec2`、`vec3` 和 `vec4` 的浮点统一变量。使用 `sampler2D` 类型的 GLSL 采样器值则通过 `FragmentShader.setImageSampler` 方法设置。

每个 `uniform` 值的正确索引由统一变量在片段程序中定义的顺序决定。对于由多个浮点数组成的数据类型(例如 `vec4`),必须为每个值调用一次 `FragmentShader.setFloat`

例如,给定 GLSL 片段程序中的以下统一变量声明:

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

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

dart
void updateShader(FragmentShader shader, Color color, Image image) {
  shader.setFloat(0, 23);  // uScale
  shader.setFloat(1, 114); // uMagnitude x
  shader.setFloat(2, 83);  // uMagnitude y

  // Convert color to premultiplied opacity.
  shader.setFloat(3, color.red / 255 * color.opacity);   // uColor r
  shader.setFloat(4, color.green / 255 * color.opacity); // uColor g
  shader.setFloat(5, color.blue / 255 * color.opacity);  // uColor b
  shader.setFloat(6, color.opacity);                     // uColor a

  // Initialize sampler uniform.
  shader.setImageSampler(0, image);
 }

请注意,与 `FragmentShader.setFloat` 一起使用的索引不包括 `sampler2D` 统一变量。此统一变量是使用 `FragmentShader.setImageSampler` 单独设置的,索引从 0 重新开始计数。

任何未初始化的浮点统一变量都将默认为 `0.0`。

当前位置

#

着色器可以访问一个 `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.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]` 范围的值的行为方式。不支持自定义平铺模式,需要在着色器中进行模拟。

性能考量

#

当目标是 Skia 后端时,加载着色器可能会很耗时,因为它必须在运行时编译为适当的平台特定着色器。如果您打算在动画期间使用一个或多个着色器,请考虑在动画开始之前预缓存片段程序对象。

您可以在不同帧之间重用 `FragmentShader` 对象;这比为每帧创建一个新的 `FragmentShader` 更高效。

有关编写高性能着色器的更详细指南,请参阅 GitHub 上的编写高效着色器

其他资源

#

如需了解更多信息,这里有一些资源。