emitter3d: How Viewer works?
https://raw.githubusercontent.com/yubrot/emitter3d/master/screenshots/with-trail.jpghttps://raw.githubusercontent.com/yubrot/emitter3d/master/screenshots/no-trail.jpg
トレイルの有無
#emitter3d において、Simulatorは各パーティクルがどの位置にどの姿勢であるかを計算するが、ViewerはこれをWebGLでスクリーン上に描画する責務を担う。特に静止画として「映える」見た目にする上でViewerは大きな役割を果たしている。Viewerの実装ではWebGLのライブラリとして最もポピュラーであろうthree.jsを用いている。 Viewerの実装は基本的にはthree.jsの Points と Mesh (パフォーマンスのためGeometry instancingを行っている) を用いた単純なものだが、パーティクルの軌跡の描画が特徴的だろう。
Viewer側で表示されるオブジェクトの状態は、一定時間ごとに(デフォルトでは毎秒60回程度)Simulator側の座標や向きなどの状態をViewer側のバッファにコピーする形で更新している。Viewerは最新の状態だけをバッファに持つのではなく、過去数十ステップの状態を保持するようにしている。この状態を元に、それぞれ現在からどれだけ古い状態かに基づいて不透明度を下げたり、座標をSimplex noiseによって拡散させて重ねて表示することで、彗星のように尾を引いた、軌跡を伴った描画が可能になる。 JavaScriptでこういうものを実装する上では、もちろん不必要な計算を削減して高速化するのも重要だが、アロケーションの頻度を減らすのが重要になる。three.jsも実装を覗いてみると徹底して新しいアロケーションが発生しないように気を遣った実装になっていることがわかる。
Viewerには大量の変更可能なパラメータがあり、描画を細かく調整できるようになっている。パラメータ群は画面右上の + から変更できる。いくつかの設定済みパラメータ群が Presets 以下に提供されている。
Viewerには重要な未解決の課題が一つある。現在のViewerはひたすら加算合成しているので、一定以上のボリュームがあるとあっさりと画面が潰れてしまう。気が向いたらemitter3dをUnityに移植して、このあたりを改善したViewerを考えてみたい。ボリュームのあるパーティクルもいい感じに描画できるViewerではパターン生成で適切な量・密度も変わってくるので、Simulator側のランダム生成も含めて再調整することになるだろう。
アプリケーション(Preact)との接続
emitter3ではアプリケーションを組み上げるのにPreactを用いている。コンポーネントの範疇にないViewerの実装をどう扱うか迷ったが、今回はHooks APIによるエフェクトとしてそれらを扱うことにした。これはどういう意味か? WebGLの描画は特定のcanvas要素上で行われるが、これを「<Viewer> のようなコンポーネントが存在し、このコンポーネント内でViewerを初期化し、Viewerへの操作はこのコンポーネントを通す」…と考えるのではなく、「Viewerエフェクト (useViewer) が存在し、Viewerエフェクトが独立してcanvas要素も所有している」と考えるようにした。
canvasを表示したいコンポーネントは、Viewerエフェクトからcanvas要素を借用する。具体的な実装としては、Screen.tsxは useViewer() でViewerのエフェクトを使用し、Viewerが保持している viewer.renderer.domElement を借用し、<Mount>コンポーネントを通して自身のDOM上にマウントしている。明示的に「借用する」といった関数を呼んでいるわけではないが... Simulatorも同様にSimulatorエフェクト(useSimulator)で扱っているので、SimulatorとViewerの接続はこれらエフェクトを使用するHooks内に凝集するようになっている (このHooksをSystemと名付けている)。