torikatsu923.dev

サイト内検索

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

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

    はじめに

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

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

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

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

    Flutterのcanvasを使った線香花火!

    夏を満喫できて最高! pic.twitter.com/Xyvo8fBqLt

    — Torikatsu (@torikatsu923) August 19, 2021

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

    リポジトリ

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

    GitHub - torikatsupg/sparkler: Sparkler app made by flutter.

    作り方

    作り方は大きく以下の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で立体的な線香花火を表現するためには奥行きを表現する必要があります。 今回はピンボケを使って奥行きを表現します。

    ピンボケは計算式があるようです。

    カメラの被写界深度の計算

    この式に則って計算をすると正確な値が出せますが、その分計算コストがかかります。 今回は少しでも計算コストを減らすために、原点と火花の距離を適当な範囲で区切ってボケに使う値を出すことにしました。

      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ってやつ見つけた
    ImageFilterに比べてかなり軽そう

    — Torikatsu (@torikatsu923) August 17, 2021

    パラメータの調整

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

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

    WebGLで綺麗な線香花火を作る

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

    おわりに

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

    -––