パーティクルの軽量化について調べてみた
昨日は、hattori ∞さんの「モンスター AI」の記事でした。
なんと、モンハンライクなゲームを作っているとのこと。完成が楽しみです。
それでは、Cluster #1 Advent Calendar 2024の6日目の記事になります。
パーティクルの軽量化について調べてみたのですが、長文になってしまったので、「7.まとめ」だけ眺めてもらえれば十分です💦。
1.はじめに
(1)自己紹介
はじめまして。Clusterの2024年10月のお題企画「音楽イベント会場をつくろう!」で、「パーティクルしかないところ」というワールドで、スワンマン賞を受賞した人です。
(2)記事の内容
この受賞したワールドは、50種以上のパーティクルを使って演出することができます。
しかし、パーティクルは綺麗だけど重くなりやすいので、パーティクルの軽量化について調べてみました。
これから、パーティクルを使ったワールドを作成する人の参考になれば、幸いです。
2.負荷の確認方法
ワールドが重い場合、「ワールド容量」の問題と、「描画負荷」の問題があります。
(1)ワールド容量の確認方法
Cluster上で確認するには、ワールド説明欄にあるデータサイズが、ワールド容量になります。
このデータサイズが大きいと、入室するまでのロード時間が長くなり、メモリの消費量が大きくなるので落ちやすくなります。
今回は、詳細に検討したいので、Unity上でワールドの容量を確認します。
まず、ワールドアップロード後に、Consoleビューの右上にある三本線メニューから、OpenEditorLogを選択します。
すると、Editor.logというファイルが開くので、このファイルの中で「Bundle Name:」という文字列で検索して、以下の該当するログを見つけます。(ファイルの後ろの方にあるので、一番後ろからスライドさせて、該当するログを見つけることもできます。)
このログの中のCompressed Sizeが、データサイズに相当します。
(2)描画負荷の確認方法
Unityの再生中に、ゲームビューの右上にあるStatsをクリックすると、レンダリング統計ウィンドウ(Statistics)が表示されます。
Statisticsの主な指標を示します。
FPS:Unity が 1 秒間に描画可能な現在のフレーム数。
Batches:1 フレームで Unity が処理する ドローコールバッチ の総数。
SetPass Calls:そのフレームでゲームオブジェクトのレンダリングに使用するシェーダーパスを Unity が切り替えた回数。
Tris:Unity が 1 フレーム中に処理する 三角形 の数。
Verts :Unity が 1 フレーム中に処理する 頂点 の数。
描画工程をざっくりと説明すると、CPUがGPUに描画を指示し、GPUが描画することによりフレームが描画されます。
このため、描画負荷は、CPUの負荷と、GPUの負荷に分けられます。
上記指標のうち、BatchesとSetPass Callsの値が大きくなるほど、CPUの負荷が大きくなります。
また、TrisとVertsの値が大きくなるほど、GPUの負荷が大きくなります。
そして、CPUの負荷とGPUの負荷が大きくなると、最終的に、1 秒間に描画可能なフレーム数:FPSが小さくなり、画面がカクカクすることになります。
3.比較するワールドの構成
比較用のワールドとして、次のワールドを比較します。
(1)基本ワールド
Partile Systemのないワールド
なお、ワールドの最小構成として、Spawn Point、Despawn Height、Plane(Collider)があれば十分ですが、パーティクルの演出を考慮して、Bloomを設定したPost-process Volumeを加えています。
(2)パーティクルワールド
Particle Systemが1つあるワールド
4.ワールド容量の比較
2.(1)の方法で、各比較用のワールドの容量を比較します。
下表で示されるように、パーティクルが1つあることによって、約0.02MBだけ容量が増えました。
したがって、パーティクルによるワールド容量への影響は、非常に少ないといえます。
5.描画負荷の比較
2.(2)の方法で、各比較用のワールドの描画負荷を比較します。
まず、基本ワールドの描画負荷を示します。
なお、FPSは、ご使用の端末に依存します(以下、同様)。
次に、パーティクルワールドの描画負荷は、Particle Systemのパラメータに依存するので、様々なパラメータを変えたときの描画負荷を測定します。
(1)パーティクルの発生数
パーティクルの発生数を変えたときの描画負荷を測定します。
下図のように、初期のParticle Systemから、次のパラメータを変更します。
Mainモジュール
Start Lifetime: 1 パーティクルの寿命[秒]
Max Particles: 100000 パーティクルの最大数[個]
Emissionモジュール
Rate over Time: 1000 1秒あたりに発生するパーティクルの数[個/秒]
このとき、Start Lifetimeが1[秒]なので、Rate over Timeが、発生しているパーティクルの数を表します。
なお、Particle Systemの位置は、観察者から20[m]離れた位置に配置しています。
Rate over Timeを、0, 1000, 10000, 100000と変化させた結果を示します。
BatchesとSetPass callsは、基本ワールドと比較して、パーティクルの発生数にかかわらず、1だけ増えました。
TrisとVertsは、基本ワールドと比較して、それぞれパーティクルの発生数×2、パーティクルの発生数×4増えました。
BatchesとSetPass callsは、1だけ増えましたが、Particle Systemを多数配置しない限り、CPUの負荷は少ないことがわかります。
一方、TrisとVertsは、パーティクルの発生数に比例して増えるので、パーティクルの発生数が増えると、GPUの負荷は多くなります。
このため、GPUの負荷が多くなることによって、FPSが下がるものと考えられます。
(2)トレイルの有無
トレイルの有無に対する描画負荷を測定します。
(1)と比較して、Trailsモジュールを追加しました。
BatchesとSetPass callsは、基本ワールドと比較して、パーティクルの発生数にかかわらず、パーティクルとトレイルの、2だけ増えました。
TrisとVertsは、基本ワールドと比較して、パーティクルの発生数×25~30も増えました。
BatchesとSetPass callsは、2だけ増えましたが、Particle Systemを多数配置しない限り、CPUの負荷は少ないことがわかります。
一方、TrisとVertsは、パーティクルの発生数に比例して増えるので、パーティクルの発生数が増えると、GPUの負荷は多くなります。
このため、GPUの負荷が多くなることによって、FPSが下がるものと考えられます。
また、TrisとVertsの増え方は、(1)のトレイルがない場合と比べて、10倍程度大きいため、パーティクルの発生数が少なくても、FPSは急激に下がります。
注釈
パーティクルを表示せずに、トレイルのみを表示する場合(RendererモジュールのRender Mode: None)は、基本ワールドと比較して、Batchesは、1増えますが、SetPass callsは、2増えます。
パーティクルのマテリアルと、トレイルのマテリアルとで、同じマテリアルを使うと、基本ワールドと比較して、Batchesは、2増えますが、SetPass callsは、1増えます。
追記
hkさん(X: @livevr3)から情報提供して頂いたので、追記します。hkさん、ありがとうございました。
Tris(三角形 の数)とVerts(頂点 の数)は、(パーティクルの発生数)×(トレイルの長さ)/(トレイルの頂点間距離)に、概ね比例して増加します。
そして、TrisとVertsが増加すると、FPSが下がります。
ここで、パーティクルの速度が一定のとき、
(トレイルの長さ)=(メインモジュールのStart Lifetime)×(メインモジュールのStart Speed)×(Trails モジュールのLifetime)
(トレイルの頂点間距離)=(Trails モジュールのMinimum Vertex Distance)
で表されます。
(3)パーティクルの大きさ
パーティクルの大きさを変えたときの描画負荷を測定します。
(1)と比較して、パーティクルの大きさを1[m]から、0.1[m]に小さくしました。
Mainモジュール
Start Size: 1 → 0.1 パーティクルの大きさ([m])
(1)のStart Sizeが1[m]の場合と比較して、Batches、SetPass calls、Tris、Vertsの値は変わりませんでした。
一方、Start Sizeが0.1[m]の場合、FPSは大きくなりました。
したがって、パーティクルの大きさが小さい方が、描画負荷は小さくなることがわかります。
(4)パーティクルと観察者との距離
パーティクルと観察者との距離を変えたときの描画負荷を測定します。
(1)と比較して、パーティクルと観察者との距離を20[m]から、10[m]に近づけました。
パーティクルと観察者との距離が20[m]の場合と比較して、Batches、SetPass calls、Tris、Vertsの値は変わりませんでした。
一方、パーティクルと観察者との距離が10[m]の場合、FPSは小さくなりました。
したがって、パーティクルに近づくことによって、パーティクルの見かけの大きさが大きくなると、描画負荷は大きくなることがわかります。
(5)Culling Mode
MainモジュールのCulling Modeによって、パーティクルがオフスクリーンのときにパーティクルシステムのシミュレーションを一時停止するかどうかを選択するとされています。
しかし、私の環境では、パーティクルがオフスクリーンのとき(例えば、背後にパーティクルがあって、見えていない場合)には、どのCulling Modeを選択しても、パーティクルのない基本ワールドと、ほぼ同じ描画負荷を示しました。
注釈
パーティクルの一部がオンスクリーンのとき(パーティクルの一部が見えているとき)は、パーティクルの全部がオンスクリーンのときと、ほぼ同じ描画負荷を示しました。
(6)シェーダー
Particle Systemは、デフォルトでParticle/Standard系シェーダーが用いられています。
一方、シェーダーとして、Mobile/Particles系シェーダーを用いると、処理 が軽くなるとされています。
しかし、私の環境では、Mobile/Particles系シェーダーを用いても、描画負荷に大きな変化はありませんでした。
(7)その他のパラメータ
パーティクルの色・濃淡を変えても、描画負荷に大きな変化はありませんでした。
パーティクルの動きを変えても、描画負荷に大きな変化はありませんでした。
その他のParticle Systemのパラメータを変えても、描画負荷に変化はありませんでした。
(8)描画負荷のまとめ
・パーティクルの発生数が多いほど、描画負荷が大きくなる。
・トレイルがあると、描画負荷が大きくなる。
・パーティクルの大きさが大きいほど、描画負荷が大きくなる。
・パーティクルと観察者との距離が近く、パーティクルの見かけの大きさが大きいほど、描画負荷が大きくなる。
・パーティクルのすべてがオフスクリーンのときは、パーティクルの描画負荷はなくなる。
・その他のパラメータを変化させても、描画負荷は変わらない。
描画負荷は、主に、Tris(1 フレーム中に処理する 三角形 の数)とVerts( 1 フレーム中に処理する 頂点 の数)とが、パーティクルの発生数に比例して増加することにより、GPUの負荷が増大することが原因と考えられます。
FPSの減少は、ご使用の端末によりますが、1つの目安としては、パーティクルの数を10,000~100,000個以下に抑える必要がありそうです。
6.パーティクルの軽量化
以上の結果を踏まえると、ワールド容量への影響は、非常に少なく、パーティクルによるCPUの描画負荷は、Particle Systemを多数配置しない限り、少ないことがわかりました。
一方、パーティクルによるGPUの負荷が大きくなって、FPSが下がっていると考えられるので、GPUの負荷の軽量化について検討します。
(1)パーティクルの発生数を少なくし、大きさを小さくする
5.(1)、5.(3)、5.(4)によれば、パーティクルの発生数が多いほど、パーティクルの大きさが大きいほど、描画負荷が大きくなります。
このため、パーティクルの発生数を少なくし、パーティクルの大きさを小さくするかわりに、パーティクルの輝度を上げることによって、見た目の印象をあまり変えないまま、軽量化することが可能と考えられます。
参考:
パーティクルの輝度を上げ方
下図のように、パーティクルのマテリアルを設定します。
Shader: Particles/Standard Unlit
Rendering Mode: Additive
Color Mode: Muliply
次に、Albedoの左に、テクスチャ画像をドラッグ&ドロップし、Albedoの右のHDRを選択して、HDR Colorウィンドウを立ち上げて、(R,G,B)=(4,4,4)など、1より大きな白色とします。
これにより、パーティクルの輝度が上がり、Post-process VolumeのBloomにより、光り輝くようになります。
注釈
マテリアルのEmissionを用いて、光り輝かせることもできますが、このとき、パーティクルは、マテリアルのEmissionで設定した色に光り輝くため、Particle Systemのパラメータでは、色の変更が難しくなります。
このため、Emissionを使わずに、AlbedoのHDRで設定することをお勧めします。
(2)トレイルの頂点間距離を長くする
5.(2)によれば、トレイルは、パーティクルと比較して、描画負荷が大きくなります。
原因は、TrisとVertsの増加によるものですが、TrisとVertsは、概ね(トレイルの長さ)に比例し、(トレイルの頂点間距離)に反比例します。
したがって、(トレイルの頂点間距離)を長くすることによって、すなわち、(Trails モジュールのMinimum Vertex Distance)の値を大きくすることによって、パーティクルを軽量化することができます。
ただし、トレイルの頂点間は、直線で表示されるので、(トレイルの頂点間距離)を長くしすぎると、カクカクしたトレイルになります。
したがって、見た目の印象をあまり変えない範囲内で、(Trails モジュールのMinimum Vertex Distance)の値を大きくします。
(3)LOD
LOD を用いて、カメラからの距離に応じて、パーティクルの発生数を変えたときの描画負荷を測定します。
以下では、次のようにLODを設定しました。
LOD 0:パーティクルの発生数が100,000のParticle System
LOD 1:パーティクルの発生数が10,000のParticle System
LOD 2:パーティクルの発生数が1,000のParticle System
なお、パーティクルと観察者との距離を変えると、描画負荷が変わってしまうので、LOD間のTransitionを変更することにより、パーティクルと観察者との距離を20[m]のまま、描画負荷を測定しました。
LODを設定した場合は、パーティクルの発生数が同じのLODを設定していない場合と、ほぼ同じ描画負荷を示しました。
これにより、遠くから見たときの描画負荷を自動的に減らし、パーティクルを軽量化することができます。
(4)Occlusion Culling
Occlusion Culling を用いて、Staticなオブジェクトに隠れたパーティクルを表示しないときの描画負荷を測定します。
Staticなオブジェクトに隠れるパーティクルの発生数を、1000, 10000, 100000と変化させた結果を示します。
パーティクルがStaticなオブジェクトに隠れていて、パーティクルを表示しないときは、TrisとVertsは小さいままであるにもかかわらず、パーティクルがない場合と比較して、FPSは小さくなります。
また、パーティクルがStaticなオブジェクトに隠れておらず、パーティクルが表示されているときも、Occlusion Culling が設定されていない場合と比較して、FPSは小さくなります。
結論としては、Occlusion Culling を設定しても、パーティクルを軽量化することはできません。
7.まとめ
(1)パーティクルの負荷
・ワールド容量への影響は、非常に少ない【4.】
・CPUの描画負荷は、Particle Systemを多数配置しない限り、少ない【5.(1)】
・以下の場合、GPUの描画負荷は、多い
パーティクルの発生数が多い【5.(1)】
トレイルを用いる【5.(2)】
パーティクルの大きさが大きい【5.(3)】
パーティクルの見かけの大きさが大きい(観察者との距離が近い)【5.(4)】
・その他のパラメータを変化させても、GPUの描画負荷は変わらない【5.(7)】
(2)パーティクルの軽量化
・有効な軽量化
パーティクルの発生数を少なくし、大きさを小さくする【6.(1)】
トレイルの頂点間距離を長くする【6.(2)】
LOD【6.(3)】
・有効でない軽量化
Culling Mode【5.(5)】
Mobile/Particles系シェーダー【5.(6)】
Occlusion Culling【6.(4)】
8.あとがき
以上、パーティクルの軽量化について、書かせていただきました。
長文にお付き合いくださり、ありがとうございます。
明日のCluster #1 Advent Calendar 2024は、今年、多くのイベントで大活躍された焔さんが、イベントのスタッフについて記事にされる予定です。
楽しみにして待ちましょう。