跳到主內容

編寫和使用片段著色器

如何編寫和使用片段著色器,以便在 Flutter 應用中建立自定義視覺效果。

自定義著色器可用於提供 Flutter SDK 原生功能之外的豐富圖形效果。著色器是一種使用類似 Dart 的小型語言(稱為 GLSL)編寫並在使用者 GPU 上執行的程式。

自定義著色器透過在 pubspec.yaml 檔案中列出它們來新增到 Flutter 專案中,並使用 FragmentProgram API 獲取。

在應用程式中新增著色器

#

著色器以 .frag 為副檔名的 GLSL 檔案形式存在,必須在專案 pubspec.yaml 檔案的 shaders 部分中進行宣告。Flutter 命令列工具會將著色器編譯為相應的後端格式,並生成必要的執行時元資料。編譯後的著色器會像資產(asset)一樣包含在應用程式中。

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,
  )
}

ImageFilter API

#

片段著色器也可以與 ImageFilter API 一起使用。這允許在 ImageFiltered 類或 BackdropFilter 類中使用自定義片段著色器,以將著色器應用於已渲染的內容。ImageFilter 提供了一個建構函式 ImageFilter.shader,用於建立帶有自定義片段著色器的 ImageFilter

dart
Widget build(BuildContext context, FragmentShader shader) {
  return ClipRect(
    child: SizedBox(
      width: 300,
      height: 300,
      child: BackdropFilter(
        filter: ImageFilter.shader(shader),
        child: Container(
          color: Colors.transparent,
        ),
      ),
    ),
  );
}

當將 ImageFilterBackdropFilter 一起使用時,可以使用 ClipRect 來限制受 ImageFilter 影響的區域。如果沒有 ClipRectBackdropFilter 將應用於整個螢幕。

ImageFilter 片段著色器會自動從引擎接收一些 uniforms。索引為 0 的 sampler2D 值被設定為濾鏡輸入影像,索引為 0 和 1 的 float 值被設定為影像的寬度和高度。您的著色器必須指定此建構函式以接受這些值(例如,一個 sampler2D 和一個 vec2),但您不應在 Dart 程式碼中設定它們。

當針對 OpenGLES 時,紋理的 y 座標會被翻轉,因此當從引擎提供的紋理取樣時,片段著色器應該將 UV 取消翻轉。

glsl
#version 460 core
#include <flutter/runtime_effect.glsl>

out vec4 fragColor;

// These uniforms are automatically set by the engine.
uniform vec2 u_size;
uniform sampler2D u_texture;

void main() {
  vec2 uv = FlutterFragCoord().xy / u_size;
#ifdef IMPELLER_TARGET_OPENGLES
  // When sampling from u_texture on OpenGLES the y-coordinates will be flipped.
  uv.y = 1.0 - uv.y;
#endif
  vec4 color = texture(u_texture, uv);
  float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
  fragColor = vec4(vec3(gray), color.a);
}

編寫著色器

#

片段著色器以 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.setFloatFragmentShader.getUniformFloat 方法進行設定。使用 sampler2D 型別的 GLSL 取樣器值使用 FragmentShader.setImageSamplerFragmentShader.getImageSampler 方法進行設定。

每個 uniform 值的正確索引由片段程式中定義 uniform 值的順序決定。對於由多個浮點陣列成的資料型別(如 vec4),必須為每個值呼叫一次 FragmentShader.setFloatUniformFloatSlot.set

例如,給定 GLSL 片段程式中的以下 uniform 宣告

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

初始化這些 uniform 值的相應 Dart 程式碼如下

dart
class Foobar {
  late final UniformFloatSlot _scale;
  late final List<UniformFloatSlot> _magnitude;
  late final List<UniformFloatSlot> _color;
  late final ImageSamplerSlot _texture;

  void setUp(FragmentShader shader) {
    _scale = shader.getUniformFloat('uScale');
    _magnitude = List<UniformFloatSlot>.generate(2, (int index) {
      return shader.getUniformFloat('uMagnitude', index);
    });
    _color = List<UniformFloatSlot>.generate(4, (int index) {
      return shader.getUniformFloat('uColor', index);
    });
    _texture = shader.getImageSampler('uTexture');
  }

  void update(Color color, Image image) {
    _scale.set(23);
    _magnitude[0].set(114);
    _magnitude[1].set(83);
    _color[0].set(color.r * color.a);
    _color[1].set(color.g * color.a);
    _color[2].set(color.b * color.a);
    _color[3].set(color.a);
    _texture.set(image);
  }
}

使用 FragmentShader.setFloat 時請注意,索引不計入 sampler2D uniform。此 uniform 使用 FragmentShader.setImageSampler 單獨設定,索引從 0 重新開始。

任何未初始化的浮點型 uniform 預設值均為 0.0

Flutter 著色器編譯器生成的反射資料可以透過以下命令進行審計,以便檢視 uniform 偏移量等資訊。

shell
cd $FLUTTER
# Generate the .sl file.
`find bin/ -name impellerc` \
  --runtime-stage-metal \
  --iplr \
  --input=path/to/myshader.frag \
  --sl=foo.sl \
  --spirv=foo.spirv \
  --include=engine/src/flutter/impeller/compiler/shader_lib/ \
  --input-type=frag
# Convert the .sl file to .json
flatc \
  --json \
  ./engine/src/flutter/impeller/runtime_stage/runtime_stage.fbs \
  -- ./foo.sl
# View results
cat foo.json

當前位置

#

著色器可以訪問一個包含正在評估的特定片段的區域性座標的 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 顏色不同,後者使用 0-255 的值編碼且沒有預乘 Alpha。

取樣器

#

取樣器提供對 dart:ui Image 物件的訪問。該影像可以從解碼後的影像獲取,或者使用 Scene.toImageSyncPicture.toImageSync 從應用程式的部分內容獲取。

GLSL 中的取樣器使用示例
#
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] 範圍之外的值如何表現。不支援自定義平鋪模式(tile mode),需要在著色器中進行模擬。

toImageSync 示例
#
dart
class SDFPainter {
  SDFPainter(this.sdfShader, this.renderShader);

  FragmentShader sdfShader;
  FragmentShader renderShader;
  Image? _sdf;
  bool isDirty = false;
  double radius = 0.5;

  void paint(Canvas canvas, Size size) {
    if (_sdf == null || isDirty) {
      final recorder = PictureRecorder();
      final subCanvas = Canvas(recorder);
      final paint = Paint()..shader = sdfShader;
      sdfShader.setFloat(0, size.width);
      sdfShader.setFloat(1, size.height);
      sdfShader.setFloat(2, radius);
      subCanvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
      final picture = recorder.endRecording();
      _sdf = picture.toImageSync(size.width.toInt(), size.height.toInt());
      isDirty = false;
    }

    renderShader.setFloat(0, size.width);
    renderShader.setFloat(1, size.height);
    renderShader.setImageSampler(0, _sdf!);

    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      Paint()..shader = renderShader,
    );
  }
}

效能考量

#

針對 Skia 後端時,載入著色器可能會很昂貴,因為它必須在執行時編譯為適當的特定於平臺的著色器。如果您打算在動畫期間使用一個或多個著色器,請考慮在開始動畫之前預快取片段程式物件。

您可以跨幀重用 FragmentShader 物件;這比為每一幀建立新的 FragmentShader 更高效。

有關編寫高效能著色器的更詳細指南,請檢視 GitHub 上的 編寫高效著色器

其他資源

#

欲瞭解更多資訊,請參考以下資源。