跳至主要内容

编写和使用片段着色器

自定义着色器可用于提供超出 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) {
  // 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 时,所有精度提示都会被忽略
  • 不支持无符号整数和布尔值

Uniform

#

可以通过在 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。这与使用 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 上的 编写高效的着色器

其他资源

#

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