本記事は 【unityプロ技】 Advent Calendar 2019 16日目の記事です。
本記事で記載されたソースコードはすべてパブリックドメイン
です。
修正履歴
・12/18 インスタンシングの説明を修正
はじめに
Unityアプリを安定化させるために試行錯誤を繰り返しているyoship1639です。
社内ではフロント側の開発基盤構成選定や共通基盤開発、ロジック、物理シミュレーション、ビジュアル周り(カメラワーク、シェーダ、演出)、最適化等様々なタスクを担当しています。
Unityはゲーム開発を大幅に効率化できる素晴らしいツールですが、そのすべての最適化が自動で行われる訳ではない事はご存じかと思います。詰まるところ、開発者側が最適化の手法を知っていなければUnityアプリの安定した動作を確保することはできません。しかし、参考文献が散見されどこを見直せばいいのか分からなかったりリファレンスを熟読してもよく分からなかったりすることもあるかと思います。
近年のスマートフォンは性能が高くなりつつありますが、すべてのユーザが高性能なスペックのスマートフォンを持ち合わせている訳ではありません。ユーザのスマートフォン1台の平均使用年数は4年を超えています。つまり、平均~低スペックなスマートフォンでも安定した動作を保証できなければユーザの満足度を高めることはできないので、高速かつ安定した動作は非常に重要となります。
そこで、高速かつ安定した動作を手に入れるためにどこを見直せばいいのかを9選でまとめてみました。安定した動作を提供するために、自分は何をしたらいいのかを知り、どこを直せばいいのかを認識し、実際に見直していただければと思います。
※筆者のUnity現環境は2018.4.12fのため本記事の設定内容もそれに準じています。
※本記事は特定プラットフォーム専用の最適化の記事ではありませんがスマートフォン向けアプリに寄った記事となります。
見直すべき事9選
プロファイラをまず見る
まず一番最初にやるべき事はプロファイラを見る事です。これを見れば自分のUnityアプリのどこがボトルネックになっているのかをほぼ確実に特定できます。どこが悪さしているのかを認識するのは一番大事なところなので、絶対に見ましょう。
プロファイラは[Window] - [Analysis] - [Profiler]で確認できます。
プロファイラを起動したらRecordを押下しエディタでデバッグプレイしてください、プロファイラが動作し各種グラフを見ることができます。重要なのは、CPU Usageです。CPUがどのような使われ方をしているのかとフレームレートを知ることができます。
CPU Usageは恐らく大体の方が上図の形になっているのではないかと思います。なぜなら、比較的高性能なPCで作業しているはずなので。重要なのは4~5年前の低スペックスマートフォン実機のプロファイルです。このプロファイルが本当のプロファイル結果となります。(実機でのプロファイルのやり方等は説明しないので各自調べてください)
実機のプロファイルを見ていただくと、恐らくかなりグラフが乱れているのではないかと思います。既に乱れずに高速かつ安定に動作していたら本記事の目標を達成しているので以降の内容は参考程度に眺めていただければ幸いです。安定していない場合は、60fpsを常に下回る事を目標に見直していただければと思います。
CPU Usageのグラフの詳細と対処を説明します。
Rendering
緑色のグラフは描画全般にかかった時間を示しています。主に不透明・透明オブジェクトの描画、影処理、遅延描画処理、ポストプロセス処理、UI描画処理にかかった時間がここに含まれています。このグラフがCPUの大部分を占めている場合は、描画系の処理の仕方を見直さなければなりません。
Renderingが悪さをしていると判明した場合、次に見るべきはGPU UsageとRenderingです。GPU Usageはデフォルトで隠れているのでAdd ProfilerからGPUを選択しGPUプロファイリングを有効にしてください。すると先程説明した描画の詳細をグラフで見ることができます。
GPUプロファイルとRenderingプロファイルの詳細と対処は 6. 描画周りを見直す と 7.シェーダ・マテリアル周りを見直す、8. ライト・シャドウ周りを見直す を見ていただければと思います。
Scripts
青色のグラフはスクリプト全般にかかった時間を示しています。独自にコーディングしたスクリプトやUnityビルトインスクリプト(MonoBehaviourなど)がScriptsに含まれています。ここが乱れていたりスパイク(一瞬だけすごく時間がかかっている)が立っていたりする場合スクリプト周りを見直さなければなりません。
スクリプト周りの対処は 5. メモリ周りを見直す と 9. コーディングを見直す を見てください。
Physics
オレンジ色のグラフは物理処理全般にかかった時間を示しています。剛体(Rigidbody)、衝突判定、拘束処理、レイキャスト、トリガーなどが含まれています。このグラフは比較的乱れやすくスパイクも立ちやすいので、物理処理を多用している場合は優先的に見直した方が良いです。
物理処理周りの対処は 4. 物理周りを見直す を見てください。
Animation
水色のグラフはアニメーション全般にかかった時間を示しています。AnimatorやAnimationによるFK,IKのスキンメッシュアニメーション(ボーンアニメーション)がここに含まれています。アニメーション数が多かったりボーンやIK構造が複雑だと乱れやすくなります。
アニメーション周りの対処は 3. アニメーションを見直す を見てください。
GarbageCollector
黄土色の様なよく分からない色のグラフはガベージコレクションにかかった時間を表しています。メモリ管理が甘いとスパイクが頻繁に立ちその度に一瞬アプリが硬直します。
GC周りの対処は 5. メモリ周りを見直す を見てください。
UI
紫色のグラフはUI全般にかかった時間を示しています。説明するまでもなくUIのレイアウトや描画がここに含まれています。様々なテキストやテクスチャを動かしたりたくさん描画したりすると重くなったりします。
UI周りの対処は 2. UI周りを見直す と 6. 描画周りを見直す、 7.シェーダ・マテリアル周りを見直す を見てください。
その他(VSync, Global Illmination, Other)
上記以外のVSync, Global Illmination, Otherは基本気にしなくて大丈夫です。一応簡単に説明すると、
- Vsync : リフレッシュレートに合わせてCPUを待機させている時間
- Global Illmination : 大域照明(環境光や反射光を考慮した照明)にかかった時間。非常に重いため高精細表現をしない限り使わない
- Other : それ以外にかかった時間。分別不能な内部処理のため考慮しなくていい。
となります。
プロファイラでボトルネックが判明したら、実際に見直していきます。
1. クオリティ設定を見直す
一番最初に確認するべき事ではないですが、Unityのクオリティ設定を見直すだけでも動作が改善されます。スマートフォン向けアプリはオーバースペックな設定をするべきではないので、ぜひ見直していきましょう。
[Edit] - [Project Settings] - [Quality]を開いてください。すると以下の画面が表示されます。
設定すべきは以下の項目です。
※ケースバイケースなので自分のプロジェクトにあった設定をしてください
Pixel Light Count を小さくする。もしくは0にする
もし、プロジェクト内でライトを使っていない場合はPixel Light Countを0にしてください。ライトを扱っている場合はできる限り小さい値にしてください。これで無駄なライト処理が省かれます。
Texture Qualityを下げる
高精細なスプライトやテクスチャを必要としていない場合はHalf Res以下にしましょう。
Anisotropic Texturesをオフにする
異方性フィルタリングを扱わない場合はDisableにしましょう。
Anti Aliasingをオフにする
ジャギーが気になる場合を除いてDisableにしましょう。
Soft Particlesをオフにする
ポリゴンとの境界付近にパーティクルを描画しないのであればSoft Particleは必要ないのでオフにしましょう。
Realtime Reflection Probesをオフにする
スマートフォンアプリなら必要ないことがほとんどなのでオフにしましょう。
Resolution Scaling Fixed DPI Factorを下げる
スマートフォンは解像度が高く描画負荷も大きいため0.5くらいにしても問題ないです。
Texture Streamingをオフにする
普段使わないはずなのでオフにしましょう。
Shadowsを無効にする
もしプロジェクト内で影を扱わないのであればShadowsをDisable Shadowsにしましょう。影の計算が省かれます。
Shadow Resolutionを下げる
影の深度バッファ解像度です。高いと描画負荷も高いので低くしましょう。
Blend Weightsを少なくする
スキニングのボーンブレンド数です。精細なスキンメッシュアニメーションを求めていないのであれば1 Boneにしましょう。
VSync Countを無効にする
画面のリフレッシュレートに合わせて同期させるかの設定です。基本的に自分でプロジェクトにあったフレームレートを設定した方がよいのでDon't Syncにしましょう。次に以下のコードでフレームレートを設定できます。
Application.targetFrameRate = 60;
Particle Raycast Budgetを下げる
パーティクルの衝突レイキャストの最大数です。パーティクルの品質を求めていないのであれば4, 16辺りに設定しましょう。
2. UI周りを見直す
UIを最適化したい場合、キャンバスの仕様とバッチングというものを知らなければなりません。
静的なCanvasと動的なCanvasを分ける
UIキャンバスは、内容に変化がなければ描画負荷はほぼかかりません(内容は、位置の変更、テクスチャの差し替え、マテリアルの差し替え等です)。Unityは賢いので同じ内容のものを毎回計算し直すということをしません。逆を言えば内容に変化があったらキャンバス内すべての再構築処理が走ります。しかし、何も変化がないキャンバスというのは難しいものです。なので変化のない静的キャンバスと変化が多い動的キャンバスに分けるのが最適解となります。これにより、動的な箇所のみ再計算させることができます。見落としがちな項目なので注意しましょう。
テクスチャはできるだけまとめる
バッチングというのは、簡単に説明すると一度にまとめて描画するということです。UIは同一マテリアルかつ同一テクスチャのオブジェクトをまとめて描画することをします。これによりいくら同じテクスチャを描画しまくっても描画負荷がほぼ変わらないのです。逆を言えば、マテリアルやテクスチャが変化するとバッチングは効かなくなります。なので、なるべく同一マテリアルかつ同一テクスチャであることを心掛けなければなりません。しかし、マテリアルは共通化できてもテクスチャを共通化するのは普通できません。
UnityにはSprite Atlasという機能があります。これはテクスチャを1つにまとめてパッキングするというものです。これを使うことで複数テクスチャが1つのテクスチャになるので、あとはマテリアルの変更をしない限り、1度のバッチでまとめて描画することができるようになります。ぜひ活用しましょう。
UIは基本的に上記2つを意識するだけで十分最適化できます。
3. アニメーションを見直す
アニメーションの負荷ははボーンのスキニング負荷とほぼ同値です。つまりスキニング負荷を抑える様に意識することが重要となります。
ボーンの数を減らす
これは安直な案ですが一番効果があります。なぜならスキニング負荷が純粋に減るからです。そのため、扱うモデルのボーン構造自体が重要となります。例えば、カメラが寄ることがないのに手の指にまでボーンが入っていたりすると、指先まで見えることがないのにその分無駄な処理を行っていることになります。なので、モデルのポリゴン削減だけでなく、ボーンの削減も一緒に考えるのがいいでしょう。
IKをなるべく使わない
IK(インバースキネマティクス)は負荷の高い処理です。ボーンの先からボーンの根元まで計算を繰り返すので通常のスキニングより処理負荷がかかります。そのため、どうしてもIKが必要な場面を除いては、IKをFKに焼いてしまいIKを無くしてしまうのが良いでしょう。
画面に映る時のみアニメーションさせる
画面外にあるモデルをアニメーションさせるのは、どうしても必要な場合を除いて皆無です。なので、画面内に映る場合のみアニメーションさせるようにしましょう。この設定は簡単にでき、Skinned Mesh RendererのUpdate When Offscreenをオフにすればいいだけです。デフォルトでオフになっていますが、今一度見直しましょう。
GPUにスキニングさせる
スキニングは通常CPUが担当します。トランスフォームの階層構造を持つ行列やクォータニオンの計算をGPUに担当させるのは結構骨の折れるコーディングだからです。しかし、Unityではチェックボックス1つでGPUにスキニングを担当させることができます。行列演算に関しては右に出るものがないGPUなので、処理の高速化が望めます。
プレイヤー設定のOther SettingsにGPU Skinningという項目があるので、オンにしましょう。これで、自動的にGPUスキニングになります。(※古すぎる端末だと効果がない可能性があります)
注意点としては、逆にGPU負荷が高くなりすぎて処理が遅くなってしまうことがあります。CPUとGPUの丁度いいバランスを見つけてそれに合った設定をするようにしましょう。
4. 物理周りを見直す
物理周りを見直したい場合、物理エンジンそのものの特性をある程度知る必要があります。
Rigidbody, Collider, Jointは最低限にする
まず、Unityは物理エンジンとしてPhysXを使っています。内製のものもありますがデフォルトはPhysXです。物理エンジンは基本的に物理世界を内部で作り、RigidbodyやColliderを登録し、毎フレームシミュレーションを行い、シミュレーション結果を返します。レイキャストは個別に受け付けています。UnityのコンポーネントであるRigidbodyやColliderは内部でPhysXのRigidbodyとColliderと結びついているんですね。
そんな物理エンジンですが、想像通り基本重いです。古い端末だと顕著に重くなります。重さはRigidbodyやColliderの数に比例します。特にRigidbodyは重いです。なので、無駄な使い方をしないように心がけるのが良いです。例えば、自由落下するだけならわざわざRigidbodyを使わなくてもコードで-Y方向に移動させればいいですし、複雑な衝突をしない限り単純なコードで代用できるならなるべくそうすべきです。
当たり前のことを綴っているだけですが、一番負荷を削減できる事なので常に意識しましょう。
なるべく動かさない
実は、RigidbodyやColliderの数が多くても負荷がかからない様にする方法があります。それは、動かさないことです。物理エンジンはstaticもしくは動かないと判断したオブジェクトはsleep状態に入り無駄な物理処理を行わない様になります。なので、なるべく物理オブジェクトは動かさない様にしましょう。
シーンの途中でRigidbodyやColliderを追加しない
シーンの開始ではなく途中でRigidbodyやColliderを追加したくなる時もあるかと思いますが、なるべく控えてください。Unityコンポーネントとして追加されるだけでなく物理世界にも初期登録処理を行わなければならないので結構負荷がかかります。
なのでRigidbodyとColliderを途中で追加したい場合は、予めコンポーネントとして追加しシーンの最初に
GetComponent<Rigidbody>().isKinematic = true;
GetComponent<Collider>().enabled = false;
有効にしたい時に
GetComponent<Rigidbody>().isKinematic = false;
GetComponent<Collider>().enabled = true;
として初期化処理の負荷をシーン開始時に持っていきましょう。(長くなるのでGetComponentをインラインで書いてますがキャッシュもしくはSerialize Fieldを使ってください)
精度を下げて軽くする
Unityの物理エンジンの精度はプロジェクト設定から変更することができます。
[Edit] - [Project Settings] - [Physics]を開くと物理エンジンの設定が可能です。
基本的にはいじらない方が良いですが、精度を細かく調整したい場合は以下の4つを設定するのが良いです。
- Sleep Threshold
剛体がスリープ状態に入る運動エネルギーの閾値です。デフォルトは0.005で、この値を下回った剛体はスリープ状態に入ります。大量の剛体オブジェクトを扱い、細かな運動を求めていないのであれば、この値を上げましょう。
- Default Contact Offset
コライダー同士が衝突したと判定する距離です。デフォルトは0.01です。0に近づけすぎると物理挙動がおかしくなります。大量のコライダーを扱い、かつ精度を求めていないのであれば、この値を上げましょう。
- Default Solver Iterations
ジョイントや重なり合った剛体同士の物理的な相互作用を行うタスク(ソルバー)の処理数です。デフォルトは6で、この値を上げると不安定な物理挙動が少なくなります。この値も、物理挙動が変に見えない最低値を指定してあげましょう。
- Fixed Timestep
この設定項目は[Project Settings] - [Time]にあります。Time.fixedDeltaTimeの値であり、物理シミュレーションのフレームレートを表します。デフォルトは0.02で物理シミュレーションは50fpsであることが分かります。60fpsのゲームを想定しているのであれば0.02のままで良いですが、30fpsのゲームを想定している場合は、無駄に物理シミュレーションを行う事になるので0.04に設定してあげるのがいいでしょう。
5. メモリ周りを見直す
メモリ管理はプログラミングにおいて避けては通れない問題です、たとえそれがUnityであっても。想像すればわかる通り、Instantiateはmallocと同じでヒープに動的に領域を確保します。Destroyを行うとDisposeが走り、使用されなくなったヒープが一定サイズ以上に達すると動的にGCが走ってしまいます。スタックとヒープを意識しなければならないのはUnityでも同じなので気を付けましょう。
確保されたメモリを認識し、減らすことを心掛ける
メモリ周りを見直す前に、自分のゲームがどの程度メモリを使っているのか認識しましょう。スマートフォン向けであれば、例えばメモリ容量2GBに対して1GBを超えるようなメモリを確保していると動作が不安定になったりしますので、しっかり認識した方がいいです。
プロファイラの[Memory]項目を選択すると以下の様な表示がされると思います。これを見れば現在どのリソースがどの位のメモリが確保されているのかがわかります。(下記はEditor上のプロファイラなので本来は実機のプロファイルを見たほうがいいです)
上記のUsed Totalが全体でどの位のメモリを使っているかを示しています。この値が大きかったり乱高下していたりするとGCが頻繁に発生する事になります。
その他の項目はメモリの内訳となります。例えば上記だとTexturesが一番多く141.8MB使われていることになるので、ここを減らすことを心掛けたほうが良いとわかります。
自分のゲームのメモリ領域がどの様な使われ方をしているのかをここでしっかり認識してください。
プール・キャッシュを多用する
メモリはシーンの開始時に一気に確保・解放し、シーン中はできる限り確保・解放しないことを心掛けてください。理由は単純で、メモリの動的確保・解放による負荷やGCの発生を極限まで抑える事ができるからです。そのためにもプールやキャッシュの考え方は非常に有効です。
プール・キャッシュは予めデータを確保し(Unityでいえばシーンの開始時)、そのデータを使いまわすことを意味しています。例えば、シューティングゲームがいい例です。シューティングゲームにおいて弾は常に発生するもので、もしプールを使わず弾の発射時にInstantiateしていたら、その度に動的にメモリが確保されることになり、GCが頻発し非常によろしくないです。これを回避するには、予め画面上に表示されるであろう弾の最大数分Instantiateしておき、画面外などに弾を配置しておいて、必要な時に必要な場所に配置することが望ましいです。こうすることで弾オブジェクトの動的な確保や解放が行われることがなくなるので、GCも発生せず動作も安定します。
ぜひ、プールやキャッシュは有効活用してください。
アンロードを忘れない
意外と忘れがちな事として挙げられるのがアセットのアンロードです。他にも通信周りやファイルIO周りのアンロードも挙げられます。アンロードの何がいいかと言うと、メモリリークを無くすことができ、かつメモリの領域を増やすことができます。アンロードしないままにするとメモリ領域を圧迫し続け非常によろしくないので、注意しましょう。
6. 描画周りを見直す
Unityの中で恐らく一番重いであろう処理は描画です。描画周りの最適化は、シェーダの特性を知っていれば知っている程最適化することができます。ぜひ今一度見直しましょう。
描画に関する用語を理解する
描画に関する用語は非常に多くあります。GPUプロファイルとRenderingプロファイルでも記載されている用語とその意味を知ることは非常に大事なことなので、この機会にぜひ理解していただければと思います。Unityの描画を最適化する上で必要な用語は以下の通りです。マテリアルやシェーダが絡むので、詳細は 7.シェーダ・マテリアル周りを見直す で説明します。
- Opaque
不透明描画を指す。基本的な描画はこれに当てはまる。
- Transparent
透明描画を指す。不透明描画よりコストがかかる。
- Post Process
画面全体のエフェクトを指す。かなり高価なのでどうしても必要な場面以外では使用しない事を推奨。
- Batch
同一マテリアルである等のバッチング条件を満たした結合メッシュ単位の描画処理を指す。バッチ数を減らすこと自体が描画負荷を下げる事に繋がる。
- Set Pass Call
マテリアルの設定値をシェーダ側に伝える処理を指す。シーン上のマテリアル数がここに影響してくる。このコール数が少ないほどパフォーマンスが上昇する。
- Triangles
所謂ポリゴン数。頂点シェーダのパフォーマンスに影響するので、Trianglesが少ないほどパフォーマンスが良くなる。
- Vertices
頂点数。これも頂点シェーダのパフォーマンスに影響する。少ない方が良い。
オクルージョンカリングを有効活用する
オブジェクト数が多い時に有効なのはオクルージョンカリングです。これはオブジェクトの後ろに隠れているオブジェクトの描画を行わないというもので、ドローコール数自体を下げパフォーマンスを上げる事ができる非常に優れた手法です。似たカリング手法にフラスタムカリングがあります。これはカメラの描画領域外のオブジェクトを描画しないというものです。似ているので注意しましょう。
フラスタムカリングはUnityは自動で行ってくれますが、オクルージョンカリングは手順を踏まなければ自動で行ってくれません。比較的簡単に設定できるので、ぜひ設定しましょう。
まず、シーン上で動かないオブジェクトを選択します(フィールドや小物等)。次に選択したオブジェクトが遮蔽物になるか遮蔽されるものになるかを決めます。例えば、フィールド上に配置された小物は遮蔽されるもので、壁などは遮蔽物となりますね。決めたらインスペクタ上で遮蔽物はOcculuder Static, 遮蔽されるものはOccludee Staticを指定してあげましょう。Staticの隣の逆▼から選ぶことができます。
指定が終えたら、[Window] - [Rendering] - [Occlusion Culling]を選択します。するとオクルージョンカリング設定が開かれるので、そのまま[Bake]でオクルージョンエリアを作成しましょう。これで、カメラに隠れたもの(遮蔽されるもの)は描画対象外になります。
動くものには適用されないので、動かないとわかっているオブジェクトにだけ設定してあげましょう。
ポストプロセスをなるべく使わない
ポストプロセスは、アンチエイリアスやブラー、ブルームといった描画が終わった後のエフェクト効果を指します。これらはすべてピクセルシェーダで行われるので非常に高価な処理となります(画面全体の1ピクセル毎にエフェクト効果を適用するイメージ)。そのため、スマートフォン向けアプリではできる限り使うべきではありません。見た目はよくなりますが非常に重いです。パフォーマンスとグラフィックのトレードオフの代表格なので注意しましょう。
モデルにLODを適用し無駄な頂点シェーダ処理を省く
モデルの描画に使用されるシェーダは基本、頂点シェーダとピクセルシェーダ(フラグメントシェーダ)に分かれています(それ以外もあります)。頂点シェーダのパフォーマンスはモデルの頂点数に依存し、ピクセルシェーダのパフォーマンスは画面に映るピクセル数と解像度に依存します。
しかし、例えばモデルがすごく遠くに描画された場合を考えてみましょう。ピクセルシェーダはモデルのピクセル数が減るので処理負荷も減りますが、頂点シェーダはモデルの頂点数に依存するので負荷は下がりません。これは非常に無駄な処理となります。
そこで、有効となるのがモデルにLOD(Level Of Detail)を適用することです。LODはカメラからの距離によってモデルのメッシュを使い分ける事で、遠くに行けば行くほどローポリのモデルを使います。これにより無駄な頂点シェーダを無くしパフォーマンスを向上させることができます。
ゲームオブジェクトのコンポーネントとして[LOD Group]を追加し、それぞれの距離に応じたメッシュを指定してあげればOKです。メッシュを複数用意しなくてはならないので少し大変ではありますが、ぜひ活用してみてはいかがでしょうか。
7. シェーダ・マテリアル周りを見直す
描画に関連するものの中で、マテリアルやシェーダに関連する項目はここで説明します。描画というのは、内部でマテリアルデータを使ってシェーダプログラムを走らせることを指すので、マテリアルやシェーダの扱いというのは非常に大事になります。ぜひ見直していただければと思います。
なるべくマテリアル(シェーダ)を統一する
マテリアルを扱う上で一番大事なのは、マテリアルを可能な限り統一することです。これは非常に重要なので留意してください。なぜ重要かというと、共通のマテリアルを使うことでBatchがまとまり、マテリアル切り替えによるSetPass Callが純粋に減るからです。つまり、シェーダプログラムを走らせる回数が少なくなるのでパフォーマンスも良くなります。逆に、マテリアルやシェーダがバラバラだとBatchやSetPass Callがその分増えるのでパフォーマンスは落ちます。
シェーダを扱う方であれば分かる事ですが、マテリアルというのはシェーダ(GPU)に渡すパラメータを指し、シェーダはそのままシェーダプログラムの事を指します。ドローコール(シェーダプログラムを実行する事)はパラメータ、メッシュ、シェーダが変わる度に呼ばなければならないので、パラメータは少なく、メッシュはまとめて、シェーダ切り替えも少なくする事が最適化の一番の近道ということになります。
なので、テクスチャを1つにまとめるのが良いというのは上記の理由からなのです。
インスタンシングを使う
Batchを少なくする事(メッシュをまとめる事)が大事だと先ほど述べましたが、Unityは自動的にはメッシュをまとめてくれません。まとめるにはインスタンシングというものを有効にしなければなりません。インスタンシングとは、同一マテリアルのオブジェクトのメッシュインデックスをつなげる事を指し、詰まるところメッシュをまとめるということになります。
※12/18追記、バッチとインスタンシングの説明がごっちゃになっていたので修正します。
Unityはdynamic batchを有効にするとまとめる事の出来るメッシュインデックス配列を動的につなげる事で1つのバッチにします。メッシュは同一メッシュでなくても構いません。この処理はCPU上で行われます。一方、インスタンシングを有効にすると、GPU内に同一メッシュのtransformリストを生成し、このリストを使ってシェーダを走らせることであたかも1バッチで描画しているようにします。この処理はGPU内部のみで行われるためCPU負荷がかかりません。バッチはCPUでインデックスを動的につなげるのでCPU負荷がかかりやすいので注意してください。
インスタンシングは簡単にでき、マテリアルのチェックボックスにチェックを入れればいいだけです。
ビルトインシェーダであれば[Enable GPU Instancing]という項目があるのでチェックを入れてあげましょう。ただし注意点として、同一マテリアルのオブジェクトが複数あることが前提となります。1つしかないオブジェクトなのにインスタンシングを有効にしてしまうとインスタンシングの準備コード分無駄な処理を行う事になるので逆にパフォーマンスが落ちます。注意してください。
自作シェーダの無駄を取り除く
自作シェーダを使用している場合、シェーダのパフォーマンスは描画パフォーマンスに直結しますので、無駄な処理をしていないかぜひ見直してください。
- 精度を下げる
floatは高精度で使いたくなりますがシェーダでfloatが必要な場合は意外と少ないです。スタンドアロンであればfloatでもパフォーマンスに影響はあまり出ないですが、スマートフォンだとシェーダの精度でもパフォーマンスに影響が出やすいので、なるべく下げる様にしましょう。
基本的に以下の様に指定してあげるのが良いです。
頂点データや法線データ, 計算関連:half
色データ:fixed
- if, forを展開する
GPUはifやforが苦手です。最近は使用しても処理の影響が少なくなってきましたが、それでも使うのはできるだけ避けたほうが良いです。解決法としては以下の様にするのが良いでしょう。
if:lerpやstepで代用
for:unrollして展開してしまう
- ピクセルシェーダの計算を頂点シェーダに持ってくる
頂点シェーダに対してピクセルシェーダは計算量が多いことがほとんどです。頂点シェーダは頂点数依存ですが、ピクセルシェーダは画面解像度依存だからです。例えば解像度1080x1920で画面いっぱいにモデルを描画するとなると約200万のピクセルシェーダが走ることになります。それならば、多くても数万レベルの頂点シェーダにピクセルシェーダで行う予定だった処理を持って行けば約1/100で済むことになりますね。ライティングは頂点シェーダに持っていけることがほとんどなので、移動しても見た目に影響が出ないのであれば移動させましょう。
- 高価な関数をなるべく使わない
シェーダには処理の重い関数があります。それらを認識した上でコーディングする事をお勧めします。1ピクセルに対し1回以上下記の関数を使わないようにしましょう。
処理の重い関数:pow, exp, log, cos, sin, tan
なるべくモバイルに最適化されたビルトインシェーダを使う
Unityにはモバイルに最適化されたシェーダが用意されています。基本的に自作シェーダより処理が軽いので、なるべく利用するようにしてください。パーティクルも同様です。
8. ライト・シャドウ周りを見直す
ライトとシャドウはどちらも重い処理です。なぜ重いかを理解して見直すようにしましょう。
無駄にライトを増やさない
シーン上のライトの数は極力少なくすることをお勧めします(正確にはモデルにリアルタイムで影響するライト数)。なぜかというと、ライトの数に比例してシェーダプログラムを走らせる回数も増えるからです。これはシェーダの性質上そうするしか正確にライトの計算ができないため、ライトの数が増えたらその分ライト計算ループ処理が増えると認識してください。ForwardAddのないシェーダであれば影響しませんが、ForwardAddのあるシェーダはもろ影響します。注意しましょう。
シャドウキャスト、レシーブシャドウは最小限に
影の影響を受ける必要のないオブジェクトはレシーブシャドウをオフにし、影を発生させる必要のないシーンではシャドウキャストをオフにしましょう。そもそも影を扱わないのであれば、ライトのShadow TypeをNo Shadowsにしてしまいましょう。
なぜ影が重いかというと、影を描画するためには影を発生させるオブジェクトの深度バッファを予め計算しなければならないからです。つまり影を発生させるというのは、モデルを影用に描画しているということになります。そして、それは影の深度バッファの解像度分負荷が増えます。だから影は重いのです。影の有無、深度バッファ解像度だけで処理速度が2~3ms変わったりします。注意しましょう。
9. コーディングを見直す
メモリ周りと関係してくるので説明が被るかもしれませんが、今一度理解を深めていただければと思います。
シーンの途中でInstantiate、Destroyを使わない
シーンの途中でInstantiate、Destroyを使わないようにしましょう。メモリの確保、解放が直に来るのでパフォーマンスとGCの関係上よろしくないです。なので、ゲームオブジェクトの追加はシーン中にはせず、シーン開始時に最大数分確保し、必要な時にgameObject.SetActive(true);
必要がなくなった時はgameObject.SetActive(false);
としましょう。
キャッシュを多用する
GetComponent、FindObject系はUpdate中に行わないようにしましょう。Update中に検索を行うのは無駄です。Unityは内部で自動的にキャッシュしてくれるなんてことはありません。自分でStartやAwake時にキャッシュし、それを使うようにしましょう。
高価な関数・計算を認識する
7.シェーダ・マテリアル周りを見直す でも説明しましたが高価な関数や計算はシェーダでもスクリプトでも変わりません。重いものは重いです。Unityマニュアルにもこう書いてあります。
超越関数 (Mathf.Sin、Mathf.Pow など)、除算、平方根は、すべて乗算の時間の 100 倍ほどかかります。大きなスケールで考えると大した時間ではないですが、それらを各フレームで何千回も呼び出すと、それは積もって大きくなります。
除算は逆数を掛けるという処理に代替できますし、長さの比較はmagnitude同士ではなくsqrMagnitude同士で比較した方がはるかに高速です。なるべく高速な処理を行うように心がけましょう。
まとめ
いかがでしたでしょうか。既に知っていた最適化手法もあれば、初めて知った手法もあったのではないでしょうか。
上記すべてを実践する必要は全くありません。見た目とのトレードオフなものがほとんどなので。自分に合った見直しを行っていただければ十分です。
簡単にまとめると、以下の様になります。
- 不必要なオブジェクト・コンポーネントは置かない
- シーン中に動的生成・破棄はなるべくしない
- 重い処理を認識してなるべく使わない
まだ紹介しきれていない手法も複数ありますが、それはまた別の機会に紹介できればと思います。
それでは。