[JavaScript] Three.jsで花火を実装して分かった、プロっぽく見せるための嘘と設計

はじめに

今日は、昨日作ったゲームボックスの差し替えゲームを実装する予定でしたが、何となく花火を作りたくなったので予定を変更して実装してみました。

簡単かと思いきや意外と難しく、別記事でも書いたように、バグにも直面して思うように実装が進まなかったです。

最初はシンプルな打ち上げ花火を実装した後、徐々作りんでいき、打ち上げる際、ボールを上空でフェードアウトしたり、 上空で普通に花火が開くのではなく透明状態から徐々に表示され、更に加速を減退させ開き切ったところで停止。

その後、落下しながらフェードアウトするという、やや凝った処理をしてます。

尚且つ、花火が広がる際に、光の粒子が拡大する等。

細かい調整が沢山必要で、思うような物はできなかったですが、それでも、プロに少しだけ近い花火はできたような気はします。

たかが花火、されど花火で、作りこんで行くと奥深いです。

1. 最初にやりたかったこと

Three.js で、よくある「打ち上げて爆発する花火」を実装。

派手な演出が目的というより、

  • 短命オブジェクトの管理
  • パーティクルの寿命
  • WebGL / Three.js でのエフェクト設計

このあたりを一度ちゃんと通しておきたかった。

最初は「点をたくさん出して動かせば花火になるだろう」と思っていたが、実際に作り始めると想像より遥かに奥が深かった。

2. つまずいた点(消えない粒子・filter罠)

最初に大きく詰まったのが「粒子が消えない」問題。

this.particles = this.particles.filter((p) => p.life > 0);

JavaScript としては自然なコードだが、Three.js 的には危険だった。

  • 配列から参照を消しただけ
  • Scene / Group から Mesh は消えていない
  • dispose も呼ばれない
  • 結果、上空に幽霊のように粒子が残る

特に透明にした時にだけ見えるのが厄介で、 最初は renderer や parent / children を疑ってかなり遠回りした。

正解は単純で、

Scene から remove → dispose → 最後に配列から消す

この順序を守らないと、Three.js では簡単にゴーストが生まれる。


3. Mesh → Points に変えた理由

最初は粒子を SphereGeometry + Mesh で作っていたが、見た目も負荷も限界が早かった。

プロっぽい花火をよく見ると、

  • 球体は使っていない
  • 実体のある「物体」を動かしていない
  • 点群として描いている

という事に気づく。

そこで、

  • MeshPoints
  • GeometryBufferGeometry
  • MeshBasicMaterialPointsMaterial

に切り替えた。

これだけで、

  • 表示負荷が大きく下がる
  • 火花が軽く見える
  • 数千粒子でも破綻しない

という効果が出た。


4. 花火が「それっぽくなる」3要素

球面分布

const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);

ランダムベクトルを normalize するだけだと、 密度が偏って「汚い球」になる。

球面座標で分布を作るだけで、 一気に「見たことある花火」になる。


減衰カーブ

物理的に正しい減速は不要だった。

const damping = Math.pow(1.0 - t, 0.05);

この値は嘘だが、見た目としては正解。

  • 最初はほぼ等速で開く
  • 途中で止まったように見える
  • その後、重力で落ちる

この「一瞬止まる間」が、花火らしさを決定づけていた。


フェード+サイズ変化

透明度とサイズを同時に変える。

opacity = Math.pow(life / maxLife, 2);
size = baseSize + spread * (1.0 - life / maxLife);
  • 広がりながら薄くなる
  • 消える直前が一番大きい

人間の目はこれを「エネルギーが拡散して消える」と錯覚する。