【Unity】パーティクルをGPU Instancingで描画してみる & 対応シェーダーを自作してみる
Unity 2018.1では、Instancing関係の機能が大幅に使い勝手良くなりました。
特にメッシュパーティクルの描画はCPUバッチングではなくGPU Instancingで行えるようになり、幾つかのケースでパフォーマンスが大幅に向上する事があります。
Unity 2018.1 でパーティクルに追加されたenable GPU Instancingを有効にしてみるテスト。無効(今まで通り)だと6~10FPSくらいしか出ないパーティクルの量でも、60FPS余裕で維持できた pic.twitter.com/wgRQ3Q2afu
— 椿 (@tsubaki_t1) 2018年1月10日
例えば、パーティクルを描画するとレンダースレッドのGfx.ProcessCommandsが凄い事になっているような場合、この項目を見直すと劇的に改善するかもしれません。*1
他にもUnity 2018ではInstancingに多くの改良点がありますが、今回はパーティクルの内容に絞って紹介します。
目次
パーティクルとバッチング
パーティクルシステムは大量にParticle(粒子)を描画するシステムです。その描画量は十や百ではなく、千、一万といった量です。
この量のメッシュを毎回する事は出来なくはないですが非常にパフォーマンスに悪いです。GPUにDrawCallを送るだけで大仕事になってしまいます。
そのため、Unityではメッシュをバッチング(CPUでメッシュを結合)したものをGPUへアプロードするアプローチを使用していました。
このアプローチであれば数万回のDrawCallは一回に抑えられます。
このバッチングのアプローチですが、あまり高いポリゴンに対して使用出来ないという問題があります。なにせCPUでメッシュを結合する関係上、あまりポリゴンが多いとかなり高い負荷がかかります。
特にパーティクルとして出力するオブジェクトがMeshの場合、驚くほど高い負荷を出す事があります。
これを確認するために、CubeとSphereのパーティクルを出力してみます。自分の環境ではSphereのパーティクルが3000個程度で30FPSを割る所を、Cubeならば12000個を超えても30は割りません。
毛玉?いいや、パーティクルだヨ
GPU Instancingとバッチング
Unity 2018.1から、Mesh ParticleはバッチングではなくGPU Instancingによるバッチングが可能になりました。
GPU Instancingは同じメッシュの複数のコピーを一度に描画する機能です。機能には幾つかの制約がありますが、バッチングとくらべてポリゴンの多いモデルを大量に描画するのに向いています。
バッファと描画するモデルを渡して、GPU側で座標等をデコード、羅列する感じです。
実際に試してみると、特にハイポリを量産した際に驚くほど高速になります。
モバイルやWebGLでも普通に使えるみたいです。
- PC (DirectX 11 & 12, OpenGLCore4.1, Metal)
- モバイル(OpenGL ES3.0+, Metal, Vulkan)
- WebGL(WebGL 2.0)
- Playstation 4 と Xbox one
この機能を先のSphereのメッシュを使用したパーティクルに適応してみると、先程は3000程度で30FPSを割っていたSphereのパーティクルで試した所、26000個のパーティクルを描画しても超余裕で描画できることが確認できます。
なお、あくまでバッチング処理をGPU側で行っているに近いので、描画自体の負荷は存在します。画面にミッシリとハイポリモデルを描画すればやはり重いです。
またQUADのような最低限のモデルの場合、逆に重くなるかもしれません。
さらに言えば、移動処理やライフタイム判定はCPUなので、沢山モジュールを使用すると重くなります。特にノイズは非常に重いので注意が必要です。
パーティクルのGPUInstancingを使用してみる
パーティクルのGPU Instancingを使用してみます。
まずパーティクルが使用するマテリアルを用意します。
シェーダーはParticles > Standard SurfaceもしくはStandard Unlitです。
次にParticleSystemで表示するモデルを設定します。
Particle SystemのRendererのRenderModeをMeshに変更し、Meshを適当なものに変更します。
RendererのMaterialを先程作成したMaterialに変更し、Enable GPU Instancingにチェックを入れておけば準備は完了です。
ゲームを再生・プロファイラーで確認した際、Instancingの項目のBatched Draw Callsが増えていれば成功です。
なおMaterialのEnable GPU Instancingの項目は無視されるっぽいです。チェックを入れていても外していても動作しました。
自作のシェーダーを使用する
せっかくInstancingが出来たので、今度は自作のシェーダーに試してみます。
例えば、パーティクルのシェーダーを、Textureを元にスキニングアニメーションするタイプのもにに差し替えたいといった場合や、単純なエフェクトを行うモデルを羅列表示するといった場合に有効かもしれません。
#Unity 2018.1でパーティクルがインスタンシング対応したので、 https://t.co/Asj8bce1G2 と組み合わせてみる。この量を出してもサクサク pic.twitter.com/mF8Cixoy59
— 椿 (@tsubaki_t1) 2018年1月13日
やることは超簡単です。
#pragma instancing_options procedural:vertInstancingSetup
と#include "UnityStandardParticleInstancing.cginc"
をシェーダーに追加するだけでOKです。
後はInstancingが有効になるように、#pragma multi_compile_instancing
とUNITY_VERTEX_INPUT_INSTANCE_ID
、UNITY_SETUP_INSTANCE_ID(v);
を設定するだけです。
実際のコード
頂点ベースで回転するだけのシェーダーを用意しました。これをパーティクルのGPU Instancingに対応させます。
最初の状態では、単純にモデルが回転します。これをパーティクルにて廻してみると、モデル単体ではなくパーティクル全体が回ってしまいます。
これはバッチングされたメッシュが回転してしまっているためです。
コードを変更してGPU Instancingに対応させます。
日本語のコメントが付いている項目が追記した項目です。
対応させると、パーティクルが生成されるようになります。またProfilerで確認するとInstancingから生成されていれば成功です。
関連
今回使用したStandard Particle Syaderについて
Unity 2018には標準搭載されています。
シェーダーでアニメーションを行うアプローチです。単純なものにしか使用できませんが、パフォーマンスはAnimatorよりも良い事が多いです。
回転シェーダーはコチラに含まれているアセットを参考に作成しました。
*1:この項目を計測する時はvsynkは無効にする