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

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

將著色器新增到應用

#

著色器以 .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 物件表示一個片段程式以及一組特定的 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,
  )
}

編寫著色器

#

片段著色器以 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.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。這與典型的 Flutter 顏色不同,Flutter 顏色使用 0-255 值編碼並具有未預乘 alpha。

取樣器

#

取樣器提供對 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 上的 編寫高效著色器

其他資源

#

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