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 类型 floatvec2vec3vec4 的浮点 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.01.0 的范围,并且具有预乘 alpha。这与使用 0-255 值编码并具有非预乘 alpha 的典型 Flutter 颜色不同。

采样器

#

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

其他资源

#

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