編寫和使用片段著色器
如何編寫和使用片段著色器,以在您的 Flutter 應用程式中建立自定義視覺效果。
自定義著色器可用於提供超出 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 物件表示一個片段程式以及一組特定的uniforms(配置引數)。可用的 uniforms 取決於著色器的定義方式。
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 時,著色器會為矩形內的所有片段進行評估。對於帶有描邊路徑的 API(如 Canvas.drawPath),著色器會為描邊線內的所有片段進行評估。某些 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,
)
}
ImageFilter API
#片段著色器也可以與 ImageFilter API 一起使用。這允許使用自定義片段著色器與 ImageFiltered 類或 BackdropFilter 類將著色器應用於已經渲染的內容。 ImageFilter 提供了一個建構函式,ImageFilter.shader,用於使用自定義片段著色器建立 ImageFilter。
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,
),
),
),
);
}
在使用 ImageFilter 與 BackdropFilter 時,可以使用 ClipRect 來限制受 ImageFilter 影響的區域。如果沒有 ClipRect,BackdropFilter 將應用於整個螢幕。
ImageFilter 片段著色器會自動從引擎接收一些 uniforms。索引 0 處的 sampler2D 值設定為過濾器輸入影像,索引 0 和 1 處的 float 值設定為影像的寬度和高度。您的著色器必須指定此建構函式才能接受這些值(例如,一個 sampler2D 和一個 vec2),但您不應從 Dart 程式碼中設定它們。
在定位 OpenGLES 時,紋理的 y 座標將被翻轉,因此片段著色器在從引擎提供的紋理中取樣時應取消翻轉 UV 座標。
#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 型別 float、vec2、vec3 和 vec4 的浮點 uniforms 使用 FragmentShader.setFloat 或 FragmentShader.getUniformFloat 方法設定。使用 GLSL 取樣器值(使用 sampler2D 型別)使用 FragmentShader.setImageSampler 或 FragmentShader.getImageSampler 方法設定。
每個 uniform 值的正確索引由片段程式中 uniform 值定義的順序確定。對於由多個浮點陣列成的的資料型別(例如 vec4),您必須為每個值呼叫 FragmentShader.setFloat 或 UniformFloatSlot.set 一次。
例如,給定片段程式中以下 uniforms 宣告
uniform float uScale;
uniform sampler2D uTexture;
uniform vec2 uMagnitude;
uniform vec4 uColor;
相應的 Dart 程式碼來初始化這些 uniform 值如下
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 重新開始。
任何未初始化的浮點 uniforms 預設值為 0.0。
Flutter 的著色器編譯器生成的資料可以透過以下命令進行稽核,以便檢視諸如 uniform 偏移量之類的資訊。
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 函式來實現。例如
#include <flutter/runtime_effect.glsl>
void main() {
vec2 currentPos = FlutterFragCoord().xy;
}
從 FlutterFragCoord 返回的值與 gl_FragCoord 不同。gl_FragCoord 提供螢幕空間座標,通常應避免使用它,以確保著色器在後端之間保持一致。在定位 Skia 後端時,對 gl_FragCoord 的呼叫將被重寫為訪問區域性座標,但對於 Impeller 來說,這種重寫是不可能的。
顏色
#沒有內建的顏色資料型別。相反,它們通常表示為 vec4,每個分量對應於 RGBA 顏色通道之一。
單個輸出 fragColor 期望顏色值歸一化為 0.0 到 1.0 的範圍,並且具有預乘 alpha。這與典型的 Flutter 顏色不同,後者使用 0-255 值編碼並且具有未預乘 alpha。
取樣器
#取樣器提供對 dart:ui Image 物件的訪問。可以使用解碼後的影像或使用 Scene.toImageSync 或 Picture.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] 範圍的值。不支援自定義瓦片模式,需要在著色器中對其進行模擬。
toImageSync 示例
#
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 上的 編寫高效著色器。
其他資源
#有關更多資訊,這裡有一些資源。
- The Book of Shaders by Patricio Gonzalez Vivo and Jen Lowe
- Shader toy,一個協作著色器遊樂場
-
simple_shader,一個簡單的 Flutter 片段著色器示例專案 -
flutter_shaders,一個簡化在 Flutter 中使用片段著色器的包