自定义着色器可用于提供 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 对象表示一个片段程序以及一组特定的 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

#

片段着色器可以通过设置 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 时,所有精度提示都被忽略
  • 不支持无符号整数和布尔值

Uniforms

#

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

具有 GLSL 类型 floatvec2vec3vec4 的浮点 uniform 使用 FragmentShader.setFloat 方法设置。使用 sampler2D 类型的 GLSL 采样器值使用 FragmentShader.setImageSampler 方法设置。

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

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

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 uniform。这个 uniform 是通过 FragmentShader.setImageSampler 单独设置的,索引从 0 开始重新计数。

任何未初始化的浮点 uniform 都将默认为 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.01.0 的范围,并且具有预乘 alpha。这与典型的 Flutter 颜色不同,Flutter 颜色使用 0-255 值编码并具有未预乘 alpha。

采样器

#

采样器提供对 dart:ui Image 对象的访问。此图像可以从解码图像中获取,也可以使用 Scene.toImageSyncPicture.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 上的 编写高效着色器

其他资源

#

更多信息,请参考以下资源。