はじめに
今回flutter_shadersを用いて遷移アニメーションを実装しました。
その中で詰まったポイントを中心にまとめた記事になります。
対象読者
- flutter_shadersの基本的な使い方を理解してる方 (この記事に基本的な説明はありません)。
setImageSamplerを2件セットしたいがやり方がわからない方。
これを読めばこれが作れる!
参照元:https://www.shadertoy.com/view/MslyDN
環境
- Flutter: 3.16.9
- Xcode: 15.2
- flutter_shaders: 0.1.2
困ったこと
2つ目のsetImageSamplerを用いて動作した実績が世にない。
前提として、上記動画のShaderを動作させるためには以下ソースコードのiChannel0(遷移元)用とiChannel1(遷移先)用の素材が必要です。
それぞれの素材をFlutter側からShader側に変数を渡すことで最終的にShaderを表現できるのがflutter_shadersの仕組みです。ここでは具体的な説明は省略します。
Shader側のコード

参照元:https://www.shadertoy.com/view/MslyDN
Flutter側のサンプルコード
import 'dart:ui';
import 'dart:ui' as ui;
import 'package:flutter_shaders/flutter_shaders.dart';
...
ui.Image? nextImage;
...
return ShaderBuilder(
assetKey: 'shaders/transition.frag', // GLSLファイル
child: widget.prev, // 遷移元素材
(context, shader, child) {
return AnimatedSampler(
child: child!,
(ui.Image image, Size size, Canvas canvas) {
shader.setFloat(0, size.width);
shader.setFloat(1, size.height);
shader.setFloat(2, _time);
shader.setImageSampler(0, image); // ①
shader.setImageSampler(1, nextImage!); // ② ここにセットしたい!!!
final paint = Paint(); paint.shader = shader;
canvas.drawRect(Offset.zero & size, paint);
},
);
},
);
iChannel0に対応するものが①のimage、iChannel1に対応するものが②のnextImageになります。
ここで大きく詰まったのが、②の遷移先素材を単純にセットできないことでした。
setImageSamplerの第二引数はui.Image型のみ受け入れ可能です。
①のsetImageSamplerの第二引数には、
AnimatedSamplerのbuilderのimageをそのままセットできます。
imageはShaderBuilderのchildで設定したものをAnimatedSampler がui.Image型に変えてくれています。
ここまでは基本的な使い方で問題ないです。
一方で、
②のsetImageSamplerの第二引数にセットするui.Image型はどう作り出すのでしょうか。
AnimatedSampler がui.Image型に変えてくれるのは①で使用するimageだけです。
だとすると、WidgetやImageの素材をui.Image型に変えてなんとか持ってこないといけないわけです。
当然ですがasで強制的に型を変えて動かすと怒られます。
調査
調査した時点で、flutter_shadersでsetImageSamplerを2件セットしてる人はいませんでした。
flutter_shadersに関する記事はいくつかあり、すべてに共通しているのが①にimageをセットしたらshader動くよね!という内容です。
公式ドキュメントでもsetImageSamplerは1件で紹介されています。
(モバイルアプリでこんな凝ったアニメーションを実装しようという発想に至らないのはよくわかります。。)
試しにAnimatedSamplerを重ねて②用のui.Imageを作り出してみましたが、ShaderBuilderがうまく動作せず断念しました。
対応
具体的なやり方が見つからなかったため、段階を踏んで型を変換し、最終的にui.Imageを作る方針で対応しました。
- 1. 遷移先の素材を
Uint8List型へ変換 - 2.
Uint8List型のデータからui.Image型に変換
1. 遷移先の素材をUint8List型へ変換
Future<ui.Image?> _convertToUiImage(Image image) async { final byteData = await image.toByteData(format: ImageByteFormat.png); final bytes = byteData?.buffer.asUint8List(); return await _decodeImageFromList(bytes!); // 下記2の実装へ }
2. Uint8List型からui.Image型へ変換
image_decoder.dartの実装を参照。
Future<ui.Image?> _decodeImageFromList(Uint8List bytes) async { final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); final ui.Codec codec = await PaintingBinding.instance.instantiateImageCodecWithSize(buffer); final ui.FrameInfo frameInfo = await codec.getNextFrame(); return frameInfo.image; }
全体フロー
以下イメージで実装しました。仕様によってカスタマイズしてみてください。
i) nextImageがnullの場合、_convertToUiImage()を実行。
ii) i)の間は遷移元の素材を表示させておく。
iii) _convertToUiImage()の戻り値がnullでなければそれをnextImageに代入。
iiii) nextImageが入ってきたらShaderBuilder実行。
動作確認
↑遷移元の素材にもshaderを適用。
さいごに
情報がなくて時間を費やした対応になりましたが、実装できた時の喜びは非常に大きかったです。
少しでも同じ境遇の方の助けになれば幸いです。
参考文献
- https://pub.dev/packages/flutter_shaders
- https://www.shadertoy.com/
- https://github.com/JhonaCodes/simple_widget_snapshot/blob/e75fc890b5657d8b7071589c4f771920fd40b54b/lib/src/widget_snapshot.dart#L74
- https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/painting/image_decoder.dart
- https://api.flutter.dev/flutter/dart-ui/FragmentShader/setImageSampler.html?_gl=1*cz5d97*_ga*MTI4NzUwOTA4LjE3MjY0NzQ1NTQ.*_ga_04YGWK0175*MTcyNjUyOTI3Ny4yLjEuMTcyNjUyOTQ1Ni4wLjAuMA..