はじめに

今回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に対応するものが①のimageiChannel1に対応するものが②のnextImageになります。

ここで大きく詰まったのが、②の遷移先素材を単純にセットできないことでした。

setImageSamplerの第二引数はui.Image型のみ受け入れ可能です。

①のsetImageSamplerの第二引数には、
AnimatedSamplerbuilderimageをそのままセットできます。
imageShaderBuilderchildで設定したものをAnimatedSamplerui.Image型に変えてくれています。
ここまでは基本的な使い方で問題ないです。

一方で、
②のsetImageSamplerの第二引数にセットするui.Image型はどう作り出すのでしょうか。
AnimatedSamplerui.Image型に変えてくれるのは①で使用するimageだけです。
だとすると、WidgetImageの素材をui.Image型に変えてなんとか持ってこないといけないわけです。
当然ですがasで強制的に型を変えて動かすと怒られます。

調査

調査した時点で、flutter_shaderssetImageSamplerを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) nextImagenullの場合、_convertToUiImage()を実行。
ii)  i)の間は遷移元の素材を表示させておく。
iii) _convertToUiImage()の戻り値がnullでなければそれをnextImageに代入。
iiii) nextImageが入ってきたらShaderBuilder実行。

動作確認

 

↑遷移元の素材にもshaderを適用。

さいごに

情報がなくて時間を費やした対応になりましたが、実装できた時の喜びは非常に大きかったです。
少しでも同じ境遇の方の助けになれば幸いです。

参考文献