跳至主要內容

撰寫和使用片段著色器

自訂著色器可用於提供 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 物件表示一個片段程式,以及一組特定的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);
}

畫布 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。這與使用 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 上的 撰寫高效能著色器

其他資源

#

如需更多資訊,這裡有一些資源。