编写和使用片段着色器

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

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

向应用程序添加着色器

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

flutter:
  shaders:
    - shaders/myshader.frag

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

来自包的着色器会添加到项目中,其中 packages/$pkgname 前缀于着色器程序的名称(其中 $pkgname 是包的名称)。

在运行时加载着色器

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

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

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

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

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)的双参数版本
  • 无法声明其他变化输入
  • 在针对 Skia 时,将忽略所有精度提示
  • 不支持无符号整数和布尔值

统一变量

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

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

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

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

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

初始化这些 uniform 值的相应 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 函数来访问当前位置。例如

#include <flutter/runtime_effect.glsl>

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

FlutterFragCoord 返回的值不同于 gl_FragCoordgl_FragCoord 提供屏幕空间坐标,通常应避免使用它以确保着色器在后端之间保持一致。当针对 Skia 后端时,对 gl_FragCoord 的调用会被重写以访问本地坐标,但 Impeller 无法进行此重写。

颜色

没有颜色的内置数据类型。相反,它们通常表示为 vec4,每个组件对应于 RGBA 颜色通道之一。

单个输出 fragColor 期望颜色值被归一化为 0.01.0 的范围内,并且它具有预乘的 alpha。这不同于典型的 Flutter 颜色,它使用 0-255 值编码且具有未预乘的 alpha。

采样器

采样器提供对 dart:ui Image 对象的访问。此图像可以从解码的图像或应用程序的一部分中获取,方法是使用 Scene.toImageSyncPicture.toImageSync

#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 上的 编写高效着色器

其他资源

有关更多信息,这里有一些资源。