📽️

[Flutter] Flutter × Shader の実装

2023/10/23に公開

先日、東京Flutterハッカソンに参加してきました!その際、チームメンバーが使っていた Shader に興味が湧いたので記事にしていきます。

Shader とは

簡単に言えば、Shader は「画面上の映像がどのように見えるか」をリアルタイムで計算するプログラムです。

例えば、光の反射や影のエフェクトや、キャラクターの服が風になびく動きなど、これらの視覚的なエフェクトを実現するのが Shader の役割です。

本記事では下記のサイトにある Shader のプログラムを使って画面に表示させるところまでをやっていきます。(約8万ほどの Shader があるそうです。)

https://www.shadertoy.com/

ここで、思った疑問が Shader と 3Dアニメーション の違いってなんだろう。 ということが頭に浮かび、ChatGPT に聞いてみました。

説明

シェーダ (Shader)

考え方: シェーダは、絵の具や筆のようなものと考えると分かりやすいです。絵の具の色や質感を変えることで、絵全体の見た目が大きく変わりますよね。シェーダも同じように、3Dオブジェクトや画面全体の見た目を変える役割を持っています。

機能: シェーダは、物体の色や質感、光の反射や影の描写など、ビジュアルエフェクトをリアルタイムで制御・変更します。

使用例: 水面の反射や透明なガラス、キャラクターの皮膚のような柔らかい質感などを実現するときに使用されます。

3Dアニメーション

考え方: 3Dアニメーションは、おもちゃのアクションフィギュアや人形劇のようなものと考えると良いでしょう。人形には関節があり、手や足を動かすことでさまざまなポーズをとらせることができます。3Dアニメーションも同じように、3Dモデル(人形のようなもの)を時間の経過とともに動かす技術です。

機能: 3Dモデルの動きやポーズを時間の経過に沿ってキーフレームで定義し、それに基づいてアニメーションを再生します。

使用例: 映画やゲームのキャラクターが歩いたり、走ったり、跳ねたりするシーンなどで使用されます。

まとめ

Shader は「絵の具や筆」のような役割で、物体やシーンの「見た目」を変えるためのツールです。
3Dアニメーションは「人形劇」のような役割で、3Dモデルの「動き」を制御する技術です。

私自身、3Dグラフィック周りの知識に疎いというのもあって Shader とは?といったところから調べまくりましたが今もそんなに理解はしきれていません。。。(興味は湧きました)

実装に入る前に

本記事ではサンプルの Shader として Voronoise を使用します。Shadertoy と Flutter では環境や制約が異なるのでこのままではエラーが発生します。(このあたりのことは後ほど!!)
ここでめちゃくちゃ沼りました。

実装

  • ディレクトリ構成図

省略しているファイルに

.
├── 省略...
├── lib
│   ├── main.dart
│   ├── shader_page.dart
│   └── shader_painter.dart
│
├── shaders
│   └── voronoise.frag
│
└── pubspec.yaml

省略しているファイルに追加や変更を加えることをしておりません。

  • pubspec.yaml にコードを追加
pubspec.yaml
flutter:
# assets: assets/hoge とするところ
  shaders:
    - shaders/voronoise.frag

パッケージの追加は特に必要ないのでしておりません。

  • Shader のコードを修正
voronoise.frag
+ #include<flutter/runtime_effect.glsl>

+ uniform vec2 uSize;
+ uniform float iTime;
+ vec2 iResolution;
+ uniform vec4 iMouse;
+ out vec4 fragColor;

vec3 hash3(vec2 p)
{
    vec3 q=vec3(dot(p,vec2(127.1,311.7)),
    dot(p,vec2(269.5,183.3)),
    dot(p,vec2(419.2,371.9)));
    return fract(sin(q)*43758.5453);
}

float voronoise(in vec2 p,float u,float v)
{
    float k=1.+63.*pow(1.-v,6.);
    
    vec2 i=floor(p);
    vec2 f=fract(p);
    
    vec2 a=vec2(0.,0.);
    for(int y=-2;y<=2;y++)
    for(int x=-2;x<=2;x++)
    {
        vec2 g=vec2(x,y);
        vec3 o=hash3(i+g)*vec3(u,u,1.);
        vec2 d=g-f+o.xy;
        float w=pow(1.-smoothstep(0.,1.414,length(d)),k);
        a+=vec2(o.z*w,w);
    }
    
    return a.x/a.y;
}

- void mainImage( out vec4 fragColor, in vec2 fragCoord )
+ void main(void)
{
+   iResolution = uSize;
+   vec2 fragCoord = FlutterFragCoord();
    
    vec2 uv=fragCoord/iResolution.xx;
    
    vec2 p=.5-.5*cos(iTime+vec2(0.,2.));
    
    if(iMouse.w>.001)p=vec2(0.,1.)+vec2(1.,-1.)*iMouse.xy/iResolution.xy;
    
    p=p*p*(3.-2.*p);
    p=p*p*(3.-2.*p);
    p=p*p*(3.-2.*p);
    
    float f=voronoise(24.*uv,p.x,p.y);
    
    fragColor=vec4(f,f,f,1.);
}

コピペ用
voronoise.frag
#include<flutter/runtime_effect.glsl>

uniform vec2 uSize;
uniform float iTime;
vec2 iResolution;
uniform vec4 iMouse;
out vec4 fragColor;

vec3 hash3(vec2 p)
{
    vec3 q=vec3(dot(p,vec2(127.1,311.7)),
    dot(p,vec2(269.5,183.3)),
    dot(p,vec2(419.2,371.9)));
    return fract(sin(q)*43758.5453);
}

float voronoise(in vec2 p,float u,float v)
{
    float k=1.+63.*pow(1.-v,6.);
    
    vec2 i=floor(p);
    vec2 f=fract(p);
    
    vec2 a=vec2(0.,0.);
    for(int y=-2;y<=2;y++)
    for(int x=-2;x<=2;x++)
    {
        vec2 g=vec2(x,y);
        vec3 o=hash3(i+g)*vec3(u,u,1.);
        vec2 d=g-f+o.xy;
        float w=pow(1.-smoothstep(0.,1.414,length(d)),k);
        a+=vec2(o.z*w,w);
    }
    
    return a.x/a.y;
}

void main(void)
{
    iResolution = uSize;
    vec2 fragCoord = FlutterFragCoord();
    
    vec2 uv=fragCoord/iResolution.xx;
    
    vec2 p=.5-.5*cos(iTime+vec2(0.,2.));
    
    if(iMouse.w>.001)p=vec2(0.,1.)+vec2(1.,-1.)*iMouse.xy/iResolution.xy;
    
    p=p*p*(3.-2.*p);
    p=p*p*(3.-2.*p);
    p=p*p*(3.-2.*p);
    
    float f=voronoise(24.*uv,p.x,p.y);
    
    fragColor=vec4(f,f,f,1.);
}

この部分は Shader のプログラムごとによって内容が変わってくるのでここに関しては今後も調査を進めていくつもりです。

uniform vec2 uSize;
uniform float iTime;
vec2 iResolution;
uniform vec4 iMouse;
out vec4 fragColor;
  • shader_painter.dart
    本記事では CustomPainter を使用しておりますが flame パッケージを用いた実装方法もあります。
shader_painter.dart
import 'dart:ui' as ui;

import 'package:flutter/material.dart';

class ShaderPainter extends CustomPainter {
  final ui.FragmentShader shader;
  final double time;

  ShaderPainter(ui.FragmentShader fragmentShader, this.time)
      : shader = fragmentShader;

  
  void paint(Canvas canvas, Size size) {
    final paint = Paint();
    shader.setFloat(0, size.width);
    shader.setFloat(1, size.height);
    shader.setFloat(2, time);
    shader.setFloat(3, 0);
    paint.shader = shader;
    canvas.drawRect(Offset.zero & size, paint);
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

  • shaderページ
shader_page.dart
import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';

import 'shader_painter.dart';

class ShaderPage extends StatefulWidget {
  const ShaderPage({super.key});

  
  State<ShaderPage> createState() => _ShaderPageState();
}

class _ShaderPageState extends State<ShaderPage> {
  late Timer timer;
  double delta = 0;
  ui.FragmentShader? shader;

  void loadMyShader() async {
    var program = await ui.FragmentProgram.fromAsset('shaders/voronoise.frag');
    shader = program.fragmentShader();
    setState(() {});

    timer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
      setState(() {
        delta += 1 / 60;
      });
    });
  }

  
  void initState() {
    super.initState();
    loadMyShader();
  }

  
  void dispose() {
    timer.cancel();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          CustomPaint(
            painter: ShaderPainter(shader!, delta),
          ),
        ],
      ),
    );
  }
}

loadMyShader 関数内で Shader の実行速度を変更できます。

  • 最後に main.dart
main.dart
import 'package:flutter/material.dart';

import 'shader_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: ShaderPage(),
    );
  }
}

これで Shader が表示されると思います。

最後に

Shader を Flutter で実装するところをやってきましたが実装中心で解説が全然できていないので今後も更新をし続けていきます。

参照

https://www.hanachiru-blog.com/entry/2020/10/15/120000
https://medium.com/flutter-community/creating-custom-shaders-in-flutter-a-step-by-step-guide-49ec86bec20d

Flutter大学

Discussion