Flutter × Shaderでクリスマスイブを祝おう!

image

この記事は、【 可茂IT塾 Advent Calendar 2023 】の24日目の記事です。

メリークリスマスみなさま!
今日は2023年12月24日。
なんだかハッピーな気分です。聖夜を祝いたいですね。
Flutterエンジニアの端くれとして、せっかくならFlutterを使って祝わせていただきます。  

この記事の内容

flutter_shadersというパッケージを使用し、ShaderをFlutterで表示する方法を紹介します。
完成品はこんな感じ。

メリークリスマス
↑に添付したものはGIFですが、本来はエンドレスのアニメーションです。

背景

先日のFlutter Kaigi2023にてFragment Shaderを使ったグラフィックスの描画に関する発表がありました。
こんなことできるんだなぁと感心したので、この機会に使ってみようと。クリスマスイブと掛け合わせてみようと。

僕が参考にさせていただいた発表はこちらです。
https://www.youtube.com/watch?v=6zLih07J3RU

Shaderとは

Shaderとは、グラフィックスレンダリングにおいて、光の効果やテクスチャーのディテールを計算し、リアルタイムでビジュアルを生成するプログラムです。
Flutterでは、カスタムペインティングやアニメーションのために使用されます。 (by ChatGPT4)

というわけなんです。
Flutterでも綺麗なグラフィックスが表示できるわけです。

使用パッケージ

flutter_shadersというパッケージを使うと、Flutter側の実装はかなり簡潔に書けます。
Flutter側は...。
読み進めていただければわかると思うのですが、クリスマスツリーのグラフィックスの実装部分は...という感じです。

FlutterでのShaderの実装手順

1.flutter_shadersのインストール

flutter pub add flutter_shaders

2.クリスマスツリーのグラフィックスの実装

project/shader/merry_christmas.fragという.fragファイルを用意します。
この実装部分は世界中の一流のシェーダー芸人の方が作成してくださったMerry Christmas!という作品を少しだけ弄っているだけです。
かなり複雑で自分で全て実装するのは厳しいです..

#version 460 core
#include <flutter/runtime_effect.glsl>

// From https://www.shadertoy.com/view/4dlGRn

#define PI 3.14159265359

out vec4 fragColor;

uniform vec2 iResolution;
uniform float iTime;

//Util Start

vec2 ObjUnion(vec2 obj0,vec2 obj1){
  if (obj0.x<obj1.x)
    return obj0;
  else
    return obj1;
}

vec3 sim(vec3 p,float s){
   vec3 ret=p;
   ret=p+s/2.0;
   ret=fract(ret/s)*s-s/2.0;
   return ret;
}

vec2 rot(vec2 p,float r){
   vec2 ret;
   ret.x=p.x*cos(r)-p.y*sin(r);
   ret.y=p.x*sin(r)+p.y*cos(r);
   return ret;
}

vec2 rotsim(vec2 p,float s){
   vec2 ret=p;
   ret=rot(p,-PI/(s*2.0));
   ret=rot(p,floor(atan(ret.x,ret.y)/PI*s)*(PI/s));
   return ret;
}

float rnd(vec2 v){
  return sin((sin(((v.y-1453.0)/(v.x+1229.0))*23232.124))*16283.223)*0.5+0.5; 
}

float noise(vec2 v){
  vec2 v1=floor(v);
  vec2 v2=smoothstep(0.0,1.0,fract(v));
  float n00=rnd(v1);
  float n01=rnd(v1+vec2(0,1));
  float n10=rnd(v1+vec2(1,0));
  float n11=rnd(v1+vec2(1,1));
  return mix(mix(n00,n01,v2.y),mix(n10,n11,v2.y),v2.x);
}

//Util End

 
//Scene Start
 
//Floor
vec2 obj0(in vec3 p){
  if (p.y<0.4)
  p.y+=sin(p.x)*0.4*cos(p.z)*0.4;
  return vec2(p.y,0);
}

vec3 obj0_c(vec3 p){
  float f=
    noise(p.xz)*0.5+
    noise(p.xz*2.0+13.45)*0.25+
    noise(p.xz*4.0+23.45)*0.15;
  float pc=min(max(1.0/length(p.xz),0.0),1.0)*0.5;
  return vec3(f)*0.3+pc+0.5;
}

//Snow
float makeshowflake(vec3 p){
  return length(p)-0.03;
}

float makeShow(vec3 p,float tx,float ty,float tz){
  p.y=p.y+iTime*tx;
  p.x=p.x+iTime*ty;
  p.z=p.z+iTime*tz;
  p=sim(p,4.0);
  return makeshowflake(p);
}

vec2 obj1(vec3 p){
  float f=makeShow(p,1.11, 1.03, 1.38);
  f=min(f,makeShow(p,1.72, 0.74, 1.06));
  f=min(f,makeShow(p,1.93, 0.75, 1.35));
  f=min(f,makeShow(p,1.54, 0.94, 1.72));
  f=min(f,makeShow(p,1.35, 1.33, 1.13));
  f=min(f,makeShow(p,1.55, 0.23, 1.16));
  f=min(f,makeShow(p,1.25, 0.41, 1.04));
  f=min(f,makeShow(p,1.49, 0.29, 1.31));
  f=min(f,makeShow(p,1.31, 1.31, 1.13));  
  return vec2(f,1.0);
}
 
vec3 obj1_c(vec3 p){
    return vec3(1,1,1);
}


//Star
vec2 obj2(vec3 p){
  p.y=p.y-4.3;
  p=p*4.0;
  float l=length(p);
  if (l<2.0){
  p.xy=rotsim(p.xy,2.5);
  p.y=p.y-2.0; 
  p.z=abs(p.z);
  p.x=abs(p.x);
  return vec2(dot(p,normalize(vec3(2.0,1,3.0)))/4.0,2);
  } else return vec2((l-1.9)/4.0,2.0);
}

vec3 obj2_c(vec3 p){
  return vec3(1.0,0.5,0.2);
}
 
//Objects union
vec2 inObj(vec3 p){
  return ObjUnion(ObjUnion(obj0(p),obj1(p)),obj2(p));
}
 
//Scene End
 
void main(){
  vec2 fragCoord = FlutterFragCoord().xy;
  vec2 vPos=-1.0+2.0*fragCoord.xy/iResolution.xy;
  // 上下反転
  vPos.y *= -1.0;
 
  //Camera animation
  vec3 vuv=normalize(vec3(sin(iTime)*0.3,1,0));
  vec3 vrp=vec3(0,cos(iTime*0.5)+2.5,0);
  vec3 prp=vec3(sin(iTime*0.5)*(sin(iTime*0.39)*2.0+3.5),sin(iTime*0.5)+3.5,cos(iTime*0.5)*(cos(iTime*0.45)*2.0+3.5));
  float vpd=1.5;  
 
  //Camera setup
  vec3 vpn=normalize(vrp-prp);
  vec3 u=normalize(cross(vuv,vpn));
  vec3 v=cross(vpn,u);
  vec3 scrCoord=prp+vpn*vpd+vPos.x*u*iResolution.x/iResolution.y+vPos.y*v;
  vec3 scp=normalize(scrCoord-prp);
 
  //lights are 2d, no raymarching
  mat4 cm=mat4(
    u.x,   u.y,   u.z,   -dot(u,prp),
    v.x,   v.y,   v.z,   -dot(v,prp),
    vpn.x, vpn.y, vpn.z, -dot(vpn,prp),
    0.0,   0.0,   0.0,   1.0);
 
  vec4 pc=vec4(0,0,0,0);
  const float maxl=80.0;
  for(float i=0.0;i<maxl;i++){
  vec4 pt=vec4(
    sin(i*PI*2.0*7.0/maxl)*2.0*(1.0-i/maxl),
    i/maxl*4.0,
    cos(i*PI*2.0*7.0/maxl)*2.0*(1.0-i/maxl),
    1.0);
  pt=pt*cm;
  vec2 xy=(pt/(-pt.z/vpd)).xy+vPos*vec2(iResolution.x/iResolution.y,1.0);
  float c;
  c=0.4/length(xy);
  pc+=vec4(
          (sin(i*5.0+iTime*10.0)*0.5+0.5)*c,
          (cos(i*3.0+iTime*8.0)*0.5+0.5)*c,
          (sin(i*6.0+iTime*9.0)*0.5+0.5)*c,0.0);
  }
  pc=pc/maxl;

  pc=smoothstep(0.0,1.0,pc);
  
  //Raymarching
  const vec3 e=vec3(0.1,0,0);
  const float maxd=15.0; //Max depth
 
  vec2 s=vec2(0.1,0.0);
  vec3 c,p,n;
 
  float f=1.0;
  for(int i=0;i<64;i++){
    if (abs(s.x)<.001||f>maxd) break;
    f+=s.x;
    p=prp+scp*f;
    s=inObj(p);
  }
  
  if (f<maxd){
    if (s.y==0.0)
      c=obj0_c(p);
    else if (s.y==1.0)
      c=obj1_c(p);
    else
      c=obj2_c(p);
      if (s.y<=1.0){
        fragColor=vec4(c*max(1.0-f*.08,0.0),1.0)+pc;
      } else{
         //tetrahedron normal   
         const float n_er=0.01;
         float v1=inObj(vec3(p.x+n_er,p.y-n_er,p.z-n_er)).x;
         float v2=inObj(vec3(p.x-n_er,p.y-n_er,p.z+n_er)).x;
         float v3=inObj(vec3(p.x-n_er,p.y+n_er,p.z-n_er)).x;
         float v4=inObj(vec3(p.x+n_er,p.y+n_er,p.z+n_er)).x;
         n=normalize(vec3(v4+v1-v3-v2,v3+v4-v1-v2,v2+v4-v3-v1));
  
        float b=max(dot(n,normalize(prp-p)),0.0);
        fragColor=vec4((b*c+pow(b,8.0))*(1.0-f*.01),1.0)+pc;
      }
  }
  else fragColor=vec4(0,0,0,1.0)+pc; //background color
}

この記事の趣旨は「Flutterでこんなこともできるよ。」ということをお伝えすることなので、.frag部分の具体的な説明は省略させていただきます。
僕はこちらの3本立ての記事を見てグラフィックスの実装部分はを勉強しました。
https://www.thedroidsonroids.com/blog/fragment-shaders-in-flutter-app-development
https://www.thedroidsonroids.com/blog/fragment-shaders-in-flutter-app-development-2
https://www.thedroidsonroids.com/blog/fragment-shaders-in-flutter-app-development-3
興味がある方は覗いてみてください。

3.pubspec.yamlに用意したflagファイルのパスを追記

今回はassetsでなくshadersです。

flutter:
  uses-material-design: true
  shaders:
    - shaders/merry_christmas.frag

4.Flutter側の実装

ShaderBuilderというWidgetを使ってShaderを描画します。
この際に以下のsetFloatようにmerry_christmas.frag側に変数を渡すことができます。
変数を渡す順番は要注意です。

shader.setFloatUniforms((uniforms) {
      uniforms
    ..setFloat(size.width)
    ..setFloat(size.height)
    ..setFloat(_currentTime.inMilliseconds.toDouble() / 1000);
});

Flutter側のコードの全量は以下です。
そんなにコード量も多くないですよね。

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_shaders/flutter_shaders.dart';

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

  
  State<MerryChristmasFrame> createState() => _MerryChristmasFrameState();
}

class _MerryChristmasFrameState extends State<MerryChristmasFrame>
    with SingleTickerProviderStateMixin {
  late Ticker _ticker;

  Duration _currentTime = Duration.zero;

  
  void initState() {
    super.initState();
    _ticker = createTicker((Duration elapsed) {
      setState(() {
        _currentTime = elapsed;
      });
    });
    _ticker.start();
  }

  
  void dispose() {
    _ticker.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return ShaderBuilder(
      assetKey: 'shaders/merry_christmas.frag',
      (_, shader, __) => AnimatedSampler(
        (_, size, canvas) {
          // 変数の受け渡し。
          // 順番通りに入れていくことになるので、順序の変更には要注意
          shader.setFloatUniforms((uniforms) {
            uniforms
              ..setFloat(size.width)
              ..setFloat(size.height)
              ..setFloat(_currentTime.inMilliseconds.toDouble() / 1000);
          });

          canvas.drawRect(
            Offset.zero & size,
            Paint()..shader = shader,
          );
        },
        child: Container(),
      ),
    );
  }
}

5.完成!

メリークリスマス
開発中も正直このアニメーションを動かしっぱなしだと、流石にPCにもかなり負荷がかかっているようでした。
このレベルのグラフィックスを現実的に表示するというのはまだ厳しそうだとは思いつつ、こんなもの実装できるのかという驚きもありました。

最後に

今回はクリスマスイブということで、flutter_shaderを使ってMerry Christmas!というShader作品を描画してみました。
ここまでリッチでなくとも、Shaderをうまく利用できる機会があるかもしれないので、一度触ってみて損はないと思います。
Flutterでクリスマスイブを祝ってみました〜という記事でした。(完)

クリスマスツリーのグラフィックス以外にも練習用に色々実装したリポジトリ(shader_sample)を置いておきます。
もし動かしてみようという方は参考にしてみてください。
https://github.com/makumaaku/playground

X(Twitter)にてFlutterを中心として技術発信もしているので、よかったらフォローお願いします!
https://twitter.com/marksaito4

参考

お知らせ

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More

タグ

Flutter (108)初心者向け (28)イベント (18)Google Apps Script (15)Nextjs (11)可茂IT塾 (8)Firebase (7)riverpod (6)React (6)ChatGPT (5)デザイン (5)新卒 (4)就活 (4)vscode (4)Figma (4)Dart (4)JavaScript (4)お知らせ (4)FlutterWeb (3)Prisma (3)NestJS (3)Slack (3)TypeScript (3)ワーケーション (3)インターン (3)設計 (2)線型計画法 (2)事例 (2)Git (2)Image (2)File (2)Material Design (2)画像 (2)iOS (2)アプリ開発 (2)React Hooks (2)tailwindcss (2)社会人 (2)大学生 (2)RSS (1)Google (1)Web (1)CodeRunner (1)個人開発 (1)Android (1)Unity (1)WebView (1)Twitter (1)フルリモート (1)TextScaler (1)textScaleFactor (1)学生向け (1)supabase (1)Java (1)Spring Boot (1)shell script (1)正規表現 (1)パワーポイント (1)趣味 (1)モンスターボール (1)CSS (1)SCSS (1)Cupertino (1)ListView (1)就活浪人 (1)既卒 (1)保守性 (1)iPad (1)シェアハウス (1)スクレイピング (1)PageView (1)画面遷移 (1)flutter_hooks (1)Gmail (1)GoogleWorkspace (1)ShaderMask (1)google map (1)Google Places API (1)GCPコンソール (1)Google_ML_Kit (1)Vercel (1)Google Domains (1)DeepLeaning (1)深層学習 (1)Google Colab (1)オンラインオフィス (1)オブジェクト指向 (1)クラスの継承 (1)ポリモーフィズム (1)LINE (1)Bitcoin (1)bitFlyer (1)コミュニティー (1)文系エンジニア (1)Freezed (1)permission_handler (1)flutter_local_notifications (1)markdown (1)GlobalKey (1)ValueKey (1)Key (1)アイコン (1)go_router (1)debug (1)datetime_picker (1)Apple Store Connect (1)FlutterGen (1)デバッグ (1)Widget Inspector (1)検索機能 (1)Shader (1)Navigator (1)メール送信 (1)Firebase App Distribution (1)Fastlane (1)Dio (1)CustomClipper (1)ClipPath (1)カスタム認証 (1)アニメーション (1)Arduino (1)ESP32 (1)経験談 (1)フリーランス (1)mac (1)csv (1)docker (1)GithubActions (1)Dialog (1)BI (1)LifeHack (1)ショートカット (1)Chrome (1)高校生 (1)キャリア教育 (1)非同期処理 (1)生体認証 (1)BackdropFilter (1)レビュー (1)getAuth (1)Algolia (1)コンサルティング (1)Symbol (1)

お知らせ

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More