【Flutter】Canvasで線香花火を作る
はじめに
こんにちは、とりかつ(@torikatsu)です。
夏も終わりに近づいてきました。みなさん夏休みはどこかいかれましたか? 私はうっかり夏休みを取り損ねたためずっと仕事でした()
今年はコロナの影響もあって花火大会や夏祭りもなかったため、夏を感じる機会が少なかったように思われます。 なんだかこのまま夏を終えるのは少し寂しいですよね...
ということでFlutterのCanvasで線香花火を作りました!
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Flutterのcanvasを使った線香花火!
— Torikatsu (@torikatsu923) August 19, 2021
夏を満喫できて最高! pic.twitter.com/Xyvo8fBqLt
今回の記事はFlutterのCanvasを使って線香花火を作る方法を紹介します。
リポジトリ
リポジトリはこちらになります。 github.com
作り方
作り方は大きく以下の3つのステップに分かれます。
- 火花の動きを再現する
- 被写界深度を考慮する
- パラメータの調整
火花の動きを再現する
線香花火という現象は無数の火花の動きによって起きています。 これより火花一個一個の動きをきちっと再現できればリアルな線香花火を作ることができると考えられます。 火花の動きはざっくり火花の移動と火花の分裂に分けて考えることができます。
火花の動き
秒後の火花の動きは運動方程式をで2階積分することで求められます。
積分した位置の式は以下のようになります。
- : 空気抵抗係数
- : 火花の速度
- : 火花の質量
- : 加速度 (鉛直方向以外にも式を使い回したいのであえて加速度で考えます)
- : 初期位置
実際に作るときは吹いている風を考えて初速度をとして扱います。
導出
またより
となります。
これをvについて解いて、
両辺を積分して、
より
両辺の指数をとって
ここでとすると
となります。のとき、火花の速度は初速度なので、
となりDの値がわかります。
よって秒後の火花の速度は
となります。
さらにで積分して
ここでのとき、初期位置をとすると
となりがわかります。
よって位置の式は以下のようになります。
火花の分裂
線香花火の火花の内側からは気泡が発生しています。その気泡が表面に達した際に破裂することあの火花の分裂が起きます。
分裂周りの計算は複雑になるので割愛します。詳細はこちらの論文を見てください。 非常にわかりやすくまとめてあります。
https://www.jstage.jst.go.jp/article/jcombsj/60/193/60_156/_pdf/-char/ja
この論文より、線香花火の主成分はカリウム化合物で、火花の分裂回数は最大8回ということがわかります。 またn回分裂したときの火花の半径、火花の寿命、分裂時の初速度は以下で求めることができます。
- : 分裂時の定数
- (m) 火の玉から飛び出す火花の半径(分裂0回目の火花の半径)
- (m2/s) カリウム化合物の熱拡散率
- (kg/m3) カリウム化合物の密度
- (N/m) カリウム化合物の表面張力
- 分裂回数
火花の半径、寿命、初速度は全てnによって決まり、nは1から8になることがわかっています。 そのためプログラム起動時に各分裂時の計算結果を定数として持つことができます。アニメーション中は膨大な数の計算をすることになるので、計算結果を使いまわせるのは非常に嬉しいですね!
分裂時の火花の初速度がわかったので、次は火花の初速の向きを考えます。 自然な分裂を表現するためには運動量保存を守る必要があります。
- 分裂前の火花の重さ
- 火花の初速度
- 火花1の初速度
- 火花2の初速度
とすると、運動量保存の式は以下になります。
これを解いて
これよりが決まればがわかります。
初速度はであることがわかっているため
となります。ベクトルの方向の成分を、方向の成分を、方向の成分をとすると、
となります。 これより、火花の初速度の2乗した値を適当に3分割し、平方根ととるとを求めることができます。
奥行きを表現する
2次元のCanvasで立体的な線香花火を表現するためには奥行きを表現する必要があります。 今回はピンボケを使って奥行きを表現します。
ピンボケは計算式があるようです。 keisan.casio.jp この式に則って計算をすると正確な値が出せますが、その分計算コストがかかります。 今回は少しでも計算コストを減らすために、原点と火花の距離を適当な範囲で区切ってボケに使う値を出すことにしました。
late final double sigma = () { final z = position.z.abs(); if (z < 0.035) return 0.00005; if (z < 0.040) return 0.0003; if (z < 0.045) return 0.0005; if (z < 0.050) return 0.0008; if (z < 0.055) return 0.001; return 0.002; }();
Canvasでボケを表現するためにMaskFilter.blur()
をPaint()
に渡します。
...
Paint()
..color = e.color
..maskFilter = MaskFilter.blur(BlurStyle.normal, e.sigma)
..strokeWidth = max(e.deameter, 0.001));
...
Canvasでぼかしをする方法を調べるとImageFilter
ばかり出てきて、MaskFilter
の情報はほとんど引っ掛からなかったので地味にハマりポイントでした。
FlutterのCanvasでImageFilter以外にblur入れれるやつないかなーってPaintのAPI眺めてたらmaskFilterってやつ見つけた
— Torikatsu (@torikatsu923) August 17, 2021
ImageFilterに比べてかなり軽そう
パラメータの調整
最後に色々なパラメータを調整して全体を整えます。 具体的には表示倍率を変更したり、初速度を計算結果より若干早くしました。
ここの記事にある時間の流れを若干遅くする方法は、計算量を減らしつつ見た目もそれっぽくなるのでかなり有効でした。 zenn.dev
あと計算量が多くなりコマ落ちしやかったので、isolate
で火花の計算を別スレッドで行うようにしたりしました。
おわりに
実際に線香花火を再現することで、家でコードを書きながら夏を満喫するという最高の体験をすることができました! ぜひみなさんも夏っぽいものをコードで再現して、夏を満喫しましょう!