【VRChat】ワールドをQuest対応させよう！ Part2：パフォーマンスを改善する

# はじめに
今回の記事は、前回のPart1の続きです。
容量削減の話から見たい方はこちらを先にご覧下さい。

https://qiita.com/Hellcat_152/items/5133e1bde526d1f83ed3

# 挨拶
ではPart2、ということで今回はパフォーマンスの改善に取り組んでいきます。
今回の内容はPC、Quest問わず有効なものになっていますので、Quest対応に興味がない方もよければご覧ください。
それでは本日もよろしくお願いします。

# パフォーマンスの最適化をする

今回はQuest2基準で話を進めるので、まずはQuest2でのプレイ環境が実際どんなものなのか確認しておきます。

とても軽いワールドで70~60FPS
普通のワールドで40FPS前後
広いワールドや演出の凝ったワールドになると20FPS以下

といった具合になります。
皆様には是非とも60FPSを目指していただきたいところですが、ワールドによっては絶対無理な場合もあります。

またアセットとして販売されているワールドは、ライトベイクなど**この先で紹介する手法が既に使われている場合もある**ので、その場合は読み飛ばしていただいて構いません。
どの程度のQuest対応を目指すか、それに合わせて適切な目標設定をしておきましょう。

## ⓪現在のパフォーマンスを確認する
最適化の効果を実感するために、先に現在のパフォーマンスの見方を確認しておきます。
まずはエディタ上部の再生ボタンを押し、プレイモードに入りましょう。

プレイモードに入ったら、いちどEscキーを押し、画面右上にある"**Stats**"をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/6143b351-a7cf-591b-ad9e-7801621e03f3.png)
すると枠で囲ったような表示が出ると思います。

このうち注目すべきポイントは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/15bd0e24-bc80-3048-7e77-dbfc51bc20db.png)

**・FPS(フレームレート)**
**・Batches / Saved by batching**
**・SetPass calls**

以上3つになります。

FPSはいいでしょう、皆さん大好き秒間フレームレートです。
___
じゃあまず**Batches**とはなんなのか。
これはザックリ言うと「**メッシュを描画した回数**」に相当し、一般的にこの値が高いほど、描画にかかる処理コストは大きくなります。
ただ必ずしもこの値がパフォーマンスに直結するか、と言われればそうでもなく...
後ほど検証結果も出すんですが、ライティングの最適化の方がよりパフォーマンスへの影響が大きいと私は思ってます。
___
次に**Saved by batching**ですが、これを説明するにはまず「**Batching**」について説明しておかねばなりません。
Unityには静的なメッシュを1度にまとめて描画する機能があり、主に「Static Batching」と「GPU Instancing」の二つが存在します。


Static Batching：同じマテリアルかつ、**静的な**オブジェクトにのみ適用可能
GPU Instancing：同じメッシュ＆マテリアルを持つオブジェクトのみに適用可能
　　　　　　　　Batchingと違い、パーティクルなど**動的な**オブジェクトにも適用可能。

という違いはありますが、要は「**メッシュをまとめて描画する機能**」だと思っていただければOKです。
して「**"Saved by"** batching」なわけですから、これは
「Batchingによって削減されたメッシュの描画回数」を表す数値になります。
ちなみにパーティクルにGPU Instancingを適用すると、この値が4ケタ超えたりします。
___

では最後、**SetPass calls**です。
これもザックリ言うと「マテリアルを描画した回数」に相当し、Batchesと同じくこの数が大きいほど処理負荷が高まる傾向にあります。
Batchesと同じように、ライティングの最適化後も重いようであれば気にしてみるといいかもしれません。


## ①負荷要因について知る
では次、最適化をすると言っても、まずは何が負荷を生むのかについて知っておかねばなりません。
どの部分がどれだけの負荷を生んでいるかはワールドによって様々ですので、ここではある程度共通してくる部分に的を絞って話を進めます。

### 1. リアルタイムライティング
Unityではオブジェクトに光を当てる方法が二つあり

・光や影をリアルタイムに生成する方法
・あらかじめ光源情報を計算し、ライトマップとして情報を乗せておく方法

それぞれ一長一短な部分があるのですが、このうち前者は一般的に負荷の高い方法として知られています。

### 2. 描画されるメッシュの数
Unityではメッシュを1つ描画するごとに最低でも1回の「**描画命令**」が発生するのですが、この数はワールドの負荷を減らすうえでとても大切な指標になります。
というもの、描画命令には毎度それなりの時間がかかるため、この数が多いとフレームの描画に待ち時間が発生し、結果FPSの低下を招いてしまうのです。

これが問題となる代表的なケースとしては、木々や草などを大量に配置しているワールドが挙げられます。
特定の方向を向くとFPSが1桁になってしまったり、まともに動くのもままならなかったり。
極端な場合ゲームを落とすことすらできなくなる場合もあります。

先ほど触れた「Batching」を活用すればある程度改善できるので、後程見ていきましょう。

### 3. スクリプトの負荷
これは主にフライトワールドやドライブワールドで問題になる部分です。
シーンに恒常的な演算負荷を発生させるスクリプトが複数存在することで、CPUに負荷がかかり、描画命令の待機時間を増加させてしまう要因になるのです。

Sacchan_vrc氏作のSacc's Flight And Vehicles Prefabを例にとると、10機を超えたあたりから負荷を感じ始め、20機を超えると顕著に重くなる...といった具合です。

これに関しては割とどうしようもないので、出来ることと言えばせいぜい機数を減らすかその他の部分で軽量化を詰めるくらいです。


## ②最適化を進める
負荷要因について知って頂けたところで、次は実際に最適化を進めていきます。

### 1. ライティングの軽量化
先ほど「リアルタイムな光源処理は重い」的なことを言いましたが、ならどうすればいいのか。
リアルタイムに計算することが問題なのだから、事前に計算をしてしまえばいいのです。

この「事前計算をして光源情報を焼いておく」ことこそが「**ライトベイク**」と呼ばれるものです。
陰影情報を固定してしまうため、動的なオブジェクトには向かないものの、こと室内など動きのないワールドにおいてはとても有効な手段になります。

### 2. ライトベイクをする

最初に下準備として、ワールドに使っている3DモデルのライトマップUVを生成します。
シーン上にあるオブジェクトが含まれるFBXファイルを選択し、以下のような設定をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/4dd81909-2904-f878-5967-33d888d2b680.png)
・**Generate Lightmap UVs** にチェックを入れる
・**Margin Method**を(なっていない場合は)**Calculate**に設定する
・**Min Lightmap Resolution**を**10~20**に

以上の設定が終わったらApplyを押して適用しておきましょう。

では、本腰を入れて取り掛かっていきます。
まずはシーン上に存在するライトの一覧を見るため、画面上側のツールバーから
Window->Rendering->Lighting Explorer と進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/e159a4e5-d6b1-9ecf-e011-53317017ed1e.png)

このワールドにはありませんが、もし"Mode"の項目が「**Realtime**」になっているライトが一つでも存在するなら、それらはすべて「**Baked**」に変えてしまいましょう。

次に、今「Baked」に設定したライトのそれぞれの設定に飛び、「**Culling Mask**」を「**Environment**」のみにします。
![スクリーンショット 2024-09-03 144155.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/1d27fae2-5dcb-f231-ffcc-4a5c61eabdb1.png)
これは**レイヤーが「Environment」に設定されたオブジェクトにのみ**ライトベイクをする、という設定です。

ただワールドに置かれるオブジェクトのレイヤーは、もともとは「Default」に設定されています。
このままでは照らすものが何もないので、次は**ベイク対象にしたい**オブジェクトの設定を変更します。

:::note alert
**持てるオブジェクトなど動くものはベイク対象にしないでください**
:::

ヒエラルキーから対象のオブジェクトを選択し、画面右のInspectorから以下のように設定を変更します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/72e45770-8be9-d317-0017-1b1c6d48d17a.png)
**・レイヤーをDefault -> Environmentに**
**・右上のStaticにチェックを入れる**

レイヤーを変更することでライトの光が当たるようにし、Staticにチェックを入れることで、ライトベイクの対象として設定しています。

一通り設定が出来たら、まずはここで一回試しにライトベイクをしてみましょう。
Window -> Rendering -> Lighting と進み、
開いたウィンドウの中でScene -> Lightmap Settings -> New をクリックすることで、ベイク用の設定が生成されます。

主に変えるべき項目は3つあり、
![スクリーンショット 2024-09-03 160126.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/629a3183-a291-46ff-b342-39ea862545f0.png)

・Lightmapperを**GPU**に変更
・Lightmap Resolutionを**10~20**くらいに設定
・Lightmap Compressionを**High Quality**に

こうすることで、必要最低限の解像度で素早くベイクを済ませることが出来ます。
この時「Lightmap Resolution」は、**先ほどモデルから生成した「Min Lightmap Resolution」のサイズより大きくなるようにしましょう。**
後者が20なら前者も最低20、といった具合です。

ちなみに「Lightmap Compression」をLow Qualityとかに設定すると、Androidから見たときに面白いぐらい粗さが目立ちます。

ベイクが終わったら、シーン上を見回して変な影や破綻などがないか確認しましょう。

:::note
余裕のある方は、ここでライトマップの圧縮形式を
「**RGB(A) Compressed ASTC 4x4 block**」に変えておいてください。(Androidのみ)
:::

もし「影がぼやけている」「黒ずんでいる部分がある」という場合は、ライトマップの解像度が不足していることが考えられます。
上で設定した「Lightmap Resolution」を変えてもいいのですが、それだと問題のないオブジェクトの解像度まで上がってしまうので、今回は個別に設定をすることにします。

まずは該当するオブジェクトを選択。
Mesh Renderer -> Lightmapping -> Scale in Lightmap と進み、枠内の数値を大きくします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/f7160411-505b-8832-cc0b-3e65d0ea8c39.png)

どれくらい値を大きくするかは症状の程度によって変わり、少し気になる程度なら2~3、明確に見て取れるようであれば4以上に設定してみてください。

一通り気になるオブジェクトへの設定が済んだら、再びライトベイクを始めます。
あとは気が済むまで調整、全体の解像度を上げて仕上げ、といった具合です。

### 3. アバター・小物用のライティングを設定する
して、オブジェクトに光源情報を焼き付けたわけなんですが、勘のいい方は何かここで問題に気付くかもしれませんね。
そうです、アバターやベイク対象としなかったオブジェクトを照らすライトがないのです。

ここで取れる選択肢としては主に二つあり、

**・ベイク対象レイヤーを除外したDirectional Lightを設置する**
**・ライトプローブを設置する**

以上のどちらかになります。
前者は「アバター・小物のライティングを**リアルタイムで**行う」のに対し、
後者は「アバター・小物のライティングを**ベイク情報に基づいて**行う」
といった違いがあります。

負荷的な話をするなら後者の方が軽いのですが、アバターの影を落としたい場合やアバターの明るさを動的に変えたい場合などは前者の方法を採る必要があります。
ニーズに応じて変えましょう。

#### ①ベイク対象レイヤーを除外したDirectional Lightを設置する
まずは前者のやり方から。
といってもこれは至極簡単です。

まずは普通にライトを追加。
続けて「**Culling Mask**」からベイク対象レイヤー(今回ならEnvironment)のチェックを外し、![スクリーンショット 2024-09-07 134610.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/cca7a324-fb9b-2cd8-a902-5fe5f3341ec2.png)
最後、向きや位置を調整して完了です。

:::note
ちなみにQuest単機ではRealtime Shadowが無効化されます。
:::


#### ②ライトプローブを設置する
次に後者の説明に入るんですが、こっちは少し曲者です。

まずは完成図をPON
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/7e0a7f4a-62ae-e7e8-7bda-3adfeb3497d7.png)
分かりづらくて申し訳ないんですが、この黄色い球体と、それを結ぶピンク色の線がライトプローブの正体です。

ではまずシーンにライトプローブを追加するところから。
ヒエラルキーを右クリックし、**Light->Light Probe Group** と進むと、こんな感じに立方体状に並んで出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/7d426f1c-9d89-5f28-a6f4-fc6d2dcc8c42.png)

そしたらインスペクターから「**Edit Light Probe Positions**」を選択し、編集を始めます。
私のおすすめのやり方としては、プローブの一部を選択した後、**Ctrl+D**(複製)で広げていく方法ですね。
こんな感じに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/6896993c-b8cf-f3d3-5458-de199a16161e.png)
選択した部分は青色に変わります。

一応設置するうえでの注意点も書いときますね

**1. あんまり細かく置かないこと**
　光量の差異がないところを無駄に細かくすると、ベイク情報のサイズが大きくなります。
　影の落ちる場所など、明るさが変化するところにリソースを割きましょう。

**2. オブジェクトの中にめり込ませないこと**
　めり込んだプローブが存在すると、その周囲のアバターや小物が真っ黒になります。

これさえ守れば、というか極論「真っ暗じゃ無けりゃいい」ので、割と適当に置いてもどうにかなります。

一通り設置し終わったら再度**ライトベイクを行い**、テストビルド等で動作を確認しておきましょう。

___
以上でライティング周りの処置は終わりです、お疲れさまでした。

### 4. メッシュの描画負荷を減らす
ライトまわりの処置が終わったところで、次はメッシュに起因する描画負荷を減らす作業に入ります。
ここで出てくるのが、先ほど触れた「Batches」「SetPass calls」などの指標です。

...なのですが、ライトベイクの最適化作用が強すぎるせいか今回はあまり参考になるデータが取れませんでした()
なのでStatic Batchingについては参考までに載せておきます。

正確な描画命令の回数は「Draw Calls」というまた別の数値を見る必要があるんですが、両者の値にそこまで大きな乖離が無く、また相関関係にあることから、今回は「Batches」を指標として使います。

検証用に使うのはこんなワールドです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/be0511df-2eeb-8a9a-7e95-2662790ac614.png)

### 5. Static Batchingを設定する
まずは１つ目、Static Batchingの効果と設定方法について見ていきます。
背景や家具など静的なオブジェクトに向いた手法ですね。
Static Batchingについては上の方で解説してるので、もし「あれ何だっけ？」となったら一度見直すことをお勧めします。

まずはStatic Batchingを設定していない状態での「Batches」の値を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/291731b2-16e4-194c-1865-44245f360f65.png)
Batches：872
Saved by batching：143
こんな数値してますが、何故か処理負荷は結構軽いです。



ではStatic Batchingを設定していきます。
といってもやることは簡単で、さっきライトベイクをした時と同じく右側のStaticにチェックを入れるだけで完了します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/b94a3bd1-dfb1-d89f-ec7a-78ec85a372e9.png)

では適用後の数値を確認してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/95e6cbe6-fcf6-4cb2-b411-d8925f587b32.png)
Batches：777
Saved by batching：233

Batchesが**およそ100ほど減りました**が、依然として結構な数値をしています。
が、本当になんでか分からないのですがこの状態でも軽いです。(45~55FPS)

木や草など**大量に配置されている静的なオブジェクト**に適用すれば、もっと効果が出るかもしれませんね。

### 6. GPU Instancingを設定する
次はGPU Instancingについて見ていきます。
こちらはパーティクルや大量に配置されたオブジェクトなど、動的なオブジェクトに向いた手法です。
分かりやすい例を挙げるなら、以下の動画に出てくるようなシーンですね

https://www.youtube.com/watch?v=e5WXx4PQXpU

こちらはパーティクルとメッシュで適用方法が違うので、順番に見ていきましょう。

### 7. メッシュにGPU Instancingを適用する
ではメッシュの方から見ていきましょう。
今回はこんなサンプルシーンを用意しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/b7cac7c5-9229-de62-732e-0662d5065ec4.png)

分かりづらいですが、Unity上で球を**100個**配置したものになります。
まずは使われているシェーダーがGPU Instancingに対応しているか確認しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/90b6d99c-b7cf-77f6-f10c-2ec8984965ed.png)

Standardシェーダー以外でも「**Enable GPU Instancing**」のチェックボックスがあれば大丈夫です。
有効にする方法は簡単で、各マテリアルにこのチェックを入れていくだけです。
※同じメッシュ＆マテリアルを持つオブジェクトにしか効果がないので、なんでもかんでも入れればいいってもんでもありません。

では適用前後の値を比較してみましょう。

適用前
![スクリーンショット 2024-09-06 153724.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/c5468342-7c1b-8849-dd82-5e4d2cb6c914.png)

適用後
![スクリーンショット 2024-09-06 153738.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/4cbefce6-97c1-210c-a9ed-6a9f600d6af3.png)

Batchesの値が減り、描画コストの削減に成功しているのが分かるかと思います。

一応描画回数を減らすだけならStatic Batchingでも出来るのですが、あちらと違い、GPU Instancingでは適用したオブジェクトを個別に動かすことができます。
**Static Batchingをつけたオブジェクトは動かすことができない**ので、こちらの方がより動的なオブジェクトに向いていると言えますね。

### 8. パーティクルにGPU Instancingを適用する
続いてパーティクルについても見ていきます。
**パーティクルの描画はBatchesの数値に反映されない**ため、パーティクルを多用していると「数値上の負荷は少ないのになんかFPSが低い」なんてことが起こりかねません。
GPU Instancingについてしっかりと知り、設定しておきましょう。

まずは先程同様、使っているシェーダーがGPU Instancingに対応しているかの確認をしておきます。
例を挙げると、Unity既存の
**・Particles/Standard Surface**
**・Particles/Standard Unlit**
はGPU Instancingに対応しています。

:::note alert
**メッシュ向けのGPU Instancingに対応していても、パーティクルには非対応の場合があります！**
Standardシェーダーが最も分かりやすい例です。
カスタムシェーダーを使っている場合は個別に確認してください。
:::

今回はこんな感じのトチ狂ったパーティクルを用意しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/44fd8e66-adbf-042c-16e2-190aff06b1f2.png)
秒間10000個、5秒で消えるパーティクルです。

まずはParticle Systemの設定を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/7028a90a-f30c-c83f-634b-139cfcb7d190.png)
したらば一番下のRendererの項目を開き、

・Render Modeを「Mesh」に
・Enable Mesh GPU Instancing をONに

それぞれ設定します。
「Enable Mesh GPU Instancing」は1個目の設定をした後でないと出てこないのでご注意を。

ではこちらも適用前後での違いを見てみます。
適用前
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/913a6db2-d649-137b-ab34-0a315321d5b3.png)

適用後
![スクリーンショット 2024-09-06 162801.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2016616/bb14e416-d3bd-dddf-760a-29d41afa3d71.png)

FPSが著しく向上しているのが見て取れるかと思います。
また「Saved by batching」の値が増加していることから、数値には出ていなくとも描画回数の大幅な削減に成功していることが分かります。

もし「ちゃんと設定したのに変化がない」ということであれば、先ほど述べたように
**シェーダーがGPU Instancingに対応していない**ことが考えられます。
今一度確認をしてみてください。

(適用前をVRで描画しようものならどうなるか、お分かりですね？)

___
以上でメッシュ・パーティクルの負荷についての解説も終わりです
正直ライトベイクが強すぎて、狭いワールドでは問題にならないような部分もあるんですが、一歩上の軽さを求める場合は意識してみるといいかもしれません。

## まとめ
### まとめ
前回のPart1からここまで、拙い文章ながらご覧いただきありがとうございました。
そしてここまでの作業をやってのけた方々、まずはお疲れさまでした。

なんの計画もなしに衝動で書き始めたせいで、もともと一つの記事で済むはずのものにPart2が爆誕したり、Static Batchingに関する記述が大層おざなりになってしまっているんですが、まあ目をつぶって頂けると(

まあそんな中途半端な記事ですが、皆様がワールドのQuest対応、そしてパフォーマンスについて考える際の一助となれば幸いです。

### 言いたかったこと
ここからは少し、この記事を書いた理由や伝えたかったことについて話そうかと思います。
そんなに大事なことは書いてないので、適当に読んでいってください。

時に我々Quest勢は「あそこ行きたいけどQuest対応してない」を相当な回数経験してる訳なんですが、その中でも私にとってすごくもどかしいのが

「**あと少し容量削れば100MBに収まる！**」であったり
「**ここめっちゃ容量軽いけどPC Onlyになってる！**」という場合です。

ワールドギミックや演出、あるいは**その性質上Quest対応の難しいワールドが存在するのはもちろん理解しています。**
私とてワールド製作者ですからね。
それに、ワールド製作者といえど結局はただのプレイヤーですし、当然ながらそこに**他人の介在する余地はありません。**

ただ全てのワールドがそういった事情を抱えているわけでもないわけで、Desktopで行ってみたら案外出来そうだった、なんてことも。

じゃあどうするか。
私がそのやり方を広めればいいんです。
そうして生まれたのがこの記事です。

どこかの誰かが言ってたんです、「**欲しいものは欲しい人が作るしかない**」って

前回の記事を見ていただければ分かるんですが、パフォーマンスを気にしないのであればQuest対応は割と簡単です。
アバターと違い制限も結構緩いです。
...っていう認識が広まって、少しでもQuest対応ワールドが増えればいいなーという思いを勝手にこの記事に乗せてます。
___
では、少しばかりの妄言にお付き合いいただいたところで、今回はここで締めます。
ご覧いただきありがとうございました。

# おまけ
今までに私が作ったワールドたちです。
上側二つは本文中の参考資料に使いました。

キャンパスのワールド(23.39MB)

https://vrchat.com/home/world/wrld_99b84ff6-87ac-4d93-bd19-af611b189663

寮をイメージしたワールド(28.03MB)

https://vrchat.com/home/world/wrld_ae323197-cd2b-4101-b5e5-37cd401056dd

ホームワールド(38.57MB)

https://vrchat.com/home/world/wrld_5684236d-a589-4857-b812-90a1c2277216

航空機ワールド1個目(**216.88MB**)

https://vrchat.com/home/world/wrld_8770b59c-4c35-4478-baf9-78093e1a7b9f

航空機ワールド2個目(**140.90MB**)

https://vrchat.com/home/world/wrld_52fe0e21-d23f-48c7-89be-b61c007d9e1d