Skip to main content

编写和使用片段着色器

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

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

向应用程序添加着色器

#

以扩展名为 .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 对象表示一个片段程序以及一组特定的 uniform(配置参数)。可用的 uniform 取决于着色器的定义方式。

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) {
  // 使用着色器作为颜色源绘制矩形。
  canvas.drawRect(
    Rect.fromLTWH(0, 0, size.width, size.height),
    Paint()..shader = shader,
  );

  // 绘制一个描边矩形,着色器仅应用于位于描边内的片段。
  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)
  • 无法声明其他变化输入
  • 针对 Skia 时会忽略所有精度提示
  • 不支持无符号整数和布尔值

Uniform

#

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

使用 FragmentShader.setFloat 方法设置具有 GLSL 类型 float、vec2、vec3 和 vec4 的浮点 uniform。使用 FragmentShader.setImageSampler 方法设置使用 sampler2D 类型的 GLSL 采样器值。

每个 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

  // 将颜色转换为预乘的不透明度。
  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

  // 初始化采样器 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.0 到 1.0 的范围,并且具有预乘 alpha。这与使用 0-255 值编码并具有非预乘 alpha 的典型 Flutter 颜色不同。

采样器

#

采样器提供对 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 上的 编写高效着色器。

其他资源

#

有关更多信息,以下是一些资源。