torikatsu.dev

Flutterとかプログラミングとかガジェットとか書きます

【Flutter】Canvasで線香花火を作る

はじめに

こんにちは、とりかつ(@torikatsu)です。

夏も終わりに近づいてきました。みなさん夏休みはどこかいかれましたか? 私はうっかり夏休みを取り損ねたためずっと仕事でした()

今年はコロナの影響もあって花火大会や夏祭りもなかったため、夏を感じる機会が少なかったように思われます。 なんだかこのまま夏を終えるのは少し寂しいですよね...

ということでFlutterのCanvasで線香花火を作りました!
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

今回の記事はFlutterのCanvasを使って線香花火を作る方法を紹介します。

リポジトリ

リポジトリはこちらになります。 github.com

作り方

作り方は大きく以下の3つのステップに分かれます。

  • 火花の動きを再現する
  • 被写界深度を考慮する
  • パラメータの調整

火花の動きを再現する

線香花火という現象は無数の火花の動きによって起きています。 これより火花一個一個の動きをきちっと再現できればリアルな線香花火を作ることができると考えられます。 火花の動きはざっくり火花の移動と火花の分裂に分けて考えることができます。

火花の動き

 t秒後の火花の動きは運動方程式 F=ma tで2階積分することで求められます。

積分した位置の式は以下のようになります。

  •  k: 空気抵抗係数
  •  v: 火花の速度
  •  m: 火花の質量
  •  a: 加速度 (鉛直方向以外にも式を使い回したいのであえて加速度で考えます)
  •  x_0: 初期位置

 x(t) = -\dfrac{m}{k} \lbrace (v_0 - \dfrac{ma}{k})e^{-\frac{k}{m}t} + at + v_0 - \dfrac{ma}{k} \rbrace + x_0

実際に作るときは吹いている風を考えて初速度を v_0 = v_0 + v_{wind}として扱います。


導出

運動方程式

 F = ma-kv

また F = m \dfrac{dv}{dt}より

 m\dfrac{dv}{dt} = ma-kv

となります。

これをvについて解いて、

 \dfrac{dv}{dt} = a - \dfrac{kv}{m}

 \dfrac{dv}{dt} = -\dfrac{k}{m}(v - \dfrac{ma}{k})

 \dfrac{dv}{v - \dfrac{ma}{k}} = -\dfrac{k}{m}dt

両辺を積分して、

 \int \dfrac{1}{v - \dfrac{ma}{k}}dv = \int -\dfrac{k}{m}dt

 \int \dfrac{1}{x + a}dx = \log | x + a | + C より

 \log | v - \dfrac{ma}{k} | = -\dfrac{k}{m}t + C (Cは積分定数)

両辺の指数をとって

 v - \dfrac{ma}{k} = e^{c}e^{-\frac{k}{m}t}

ここで e^{C} = Dとすると

 v - \dfrac{ma}{k} = De^{-\frac{k}{m}t}

 v = De^{-\frac{k}{m}t}+\dfrac{ma}{k}

となります。 t=0のとき、火花の速度は初速度 v_0なので、

 v0 = D + \dfrac{ma}{k}

 D = v_0 - \dfrac{ma}{k}

となりDの値がわかります。

よって t秒後の火花の速度は

 v(t) = (v0 - \dfrac{ma}{k}) e^{-\frac{k}{m}t} + \dfrac{ma}{k}

となります。

さらに t積分して

 x(t) = \int \lbrace (v0 - \dfrac{ma}{k}) e^{-\frac{k}{m}t} + \dfrac{ma}{k} \rbrace dt

 x(t) = -\dfrac{m}{k}De^{-\dfrac{k}{m}t}+\dfrac{ma}{k}t+C_2 (C_2は積分定数)

ここで t = 0のとき、初期位置を x_0とすると

 x_0 = -\dfrac{m}{k}D + C_2

 C_2 = x_0 + \dfrac{m}{k}D

となり C_2がわかります。

よって位置の式は以下のようになります。

 x(t) = -\dfrac{m}{k}De^{-\frac{m}{k}t}+\dfrac{ma}{k}t+\dfrac{mD}{k} + x_0

 x(t) = -\dfrac{m}{k} \lbrace (v_0 - \dfrac{ma}{k})e^{-\frac{k}{m}t} + at + v_0 - \dfrac{ma}{k} \rbrace + x_0

火花の分裂

線香花火の火花の内側からは気泡が発生しています。その気泡が表面に達した際に破裂することあの火花の分裂が起きます。

分裂周りの計算は複雑になるので割愛します。詳細はこちらの論文を見てください。 非常にわかりやすくまとめてあります。

https://www.jstage.jst.go.jp/article/jcombsj/60/193/60_156/_pdf/-char/ja

この論文より、線香花火の主成分はカリウム化合物で、火花の分裂回数は最大8回ということがわかります。 またn回分裂したときの火花の半径 r_n、火花の寿命 t_n、分裂時の初速度 u_nは以下で求めることができます。

 r_n = {β}^{n}R_0

 t_n = \dfrac{ {R_0}^{2} }{k} {β}^{2n}

 u_n = \sqrt{ \dfrac{σ}{ρR_0} } {β}^{ -\frac{n-1}{2} }

  •  β = 0.5: 分裂時の定数
  •  R_0 = 10^{-4} (m) 火の玉から飛び出す火花の半径(分裂0回目の火花の半径)
  •  k = {10}^{-1} (m2/s) カリウム化合物の熱拡散率
  •  ρ = {10}^{3} (kg/m3) カリウム化合物の密度
  •  σ = {10}^{-1} (N/m) カリウム化合物の表面張力
  •  n (1 \leq n \leq 8) 分裂回数

火花の半径、寿命、初速度は全てnによって決まり、nは1から8になることがわかっています。 そのためプログラム起動時に各分裂時の計算結果を定数として持つことができます。アニメーション中は膨大な数の計算をすることになるので、計算結果を使いまわせるのは非常に嬉しいですね!

分裂時の火花の初速度がわかったので、次は火花の初速の向きを考えます。 自然な分裂を表現するためには運動量保存を守る必要があります。

  •  m 分裂前の火花の重さ
  •  v_0 火花の初速度
  •  v_1 火花1の初速度
  •  v_2 火花2の初速度

とすると、運動量保存の式は以下になります。

 mv_0 = \dfrac{1}{2}mv_1 + \dfrac{1}{2}mv_2

これを解いて

 2v_0 = v_1 + v_2

 v_2 = 2v_0 - v_1

これより v_1が決まれば v_2がわかります。

初速度は v_nであることがわかっているため

 {v_{n}}^2 = {v_1}^2

となります。ベクトルの x方向の成分を a y方向の成分を b z方向の成分を cとすると、

 {v_{n}}^2 = a^{2} + b^{2} + c^{2}

となります。 これより、火花の初速度の2乗した値を適当に3分割し、平方根ととると v_1を求めることができます。

奥行きを表現する

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の情報はほとんど引っ掛からなかったので地味にハマりポイントでした。

パラメータの調整

最後に色々なパラメータを調整して全体を整えます。 具体的には表示倍率を変更したり、初速度を計算結果より若干早くしました。

ここの記事にある時間の流れを若干遅くする方法は、計算量を減らしつつ見た目もそれっぽくなるのでかなり有効でした。 zenn.dev

あと計算量が多くなりコマ落ちしやかったので、isolateで火花の計算を別スレッドで行うようにしたりしました。

おわりに

実際に線香花火を再現することで、家でコードを書きながら夏を満喫するという最高の体験をすることができました! ぜひみなさんも夏っぽいものをコードで再現して、夏を満喫しましょう!