モバイルゲームの描画負荷制御  [Unityゲームグラフィックス実践]


　この記事は KLab Engineer Advent Calendar 2023 24日目の記事です。
Qiitaには毎年このアドベントカレンダーの12/24に投稿させて頂いていて、今回で5年目になります <A HREF="https://www.youtube.com/watch?v=rguvg-9sU2I">suzuna-honda</A> といいます。よろしくお願いします。

![clip_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/0c3ea17d-4cd2-79b7-ba91-cfce59649cd0.jpeg)

# はじめに

### スマホ、スペック差が激し過ぎる問題

　モバイル端末の性能は日進月歩で進化していて、最新のiPhoneともなると下手な据え置き機と肩を並べる程度のグラフィックスを実現できるようになってきています。

それに連動して廉価な端末のスペックも上がって...くれていれば良かったのですが、現実は非情です。iOS端末はちゃんと世代によって性能が上がってくれていますが、Android端末においては低スペック端末の性能はここ数年殆ど伸びておらず、現役で売られているスマホが10年前のハイエンドスマホよりも余裕で低性能、なんてのはザラにあります。

日本国内に限定するのであれば、その手の（ゲームを遊ぶことは想定されていない）低性能な端末は切り捨ててもそこまで大きなダメージではないかもしれません。しかしスマホの性能平均値がそこまで高くないグローバル、特に新興国をターゲットとする場合には、シェア率的に考えて低スペック端末を切り捨てる選択肢はなかなか取りにくいでしょう。

だからといって、低スペック端末に向けて絵作りを捨てたゲームを...としてしまうと、ハイスペックな端末を持つユーザー≒課金してくれる可能性の高い大事にするべきお客さんを逃すことになりかねません。

つまるところ、モバイルである程度の規模のタイトルを製作するのであれば、ハイスペック端末も低スペック端末もどちらにも対応できるように描画負荷を制御する必要があります。高低差で耳がキーンとなるくらいの極端なスペック差、どうやって吸収しましょう？


### 発熱/バッテリー消費問題

　最新端末の高性能化は著しいですが、その反面、発熱がとんでもない事になっています。何も配慮せず出せるパフォーマンスをフル活用していると、触れないほど熱くなる端末が出来上がります。この状態になると、サーマルスロットリングによる性能低下も致命傷になり得るわけです。
 
また、バッテリーもSoC等に比して性能向上を感じることは出来ず、全力で回し続けると一瞬で空っぽになってしまいます。むしろハイスペックな端末ほど高性能な分バッテリーは素早く溶けていきます。この問題についてもケアが必須となります。

なお描画とは無関係の、例えばネットワーク通信での発熱/バッテリー消費も激しかったりしますが、今回はあくまで描画周りにのみ話題を絞ります。


# 本題の前に

![_9890abe0-1297-4cba-b504-cba15fa2005c.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/0251979a-5495-18e0-c6ef-6e338c415c01.jpeg)

　以降の文章は全て、2023年時点のLTS最新であるUnity2022の最新バージョンを想定して書かれています。基本的には最近のUnity/URP環境であればそこまでバージョンに依存しない話になっているはずですが、細かい差異はあるかと思いますのでその辺りは適当に噛み砕いて読んで頂けたらと思います。


### まずは最適化を頑張る

　わざわざ書くまでもありませんが、まずは基本となる最適化を一通り済ませておきましょう。

まずはプロファイリング環境を整え、ボトルネックを見つけ出しそれを潰しましょう。最適化とはプロファイリングのことだ、なんてのは万回繰り返された話です。

無駄 / 不要な描画オブジェクト、オーバードロー、ポストプロセスは可能な限り削ります。最も有効な最適化は「何もしない」こと、なんてのは億回繰り返された話です。

もう少し具体的な話もしましょうか...
UnityのCameraは一つ増やしただけで理解不能なほどイニシャルコストが高くつくので、極力少なめに。
シェーダは（1フレに1回しか呼ばれないポストプロセス用みたいな特殊なもの以外は）全てSRP Batcherへの対応が必須。
利用するRenderTextureの解像度、フォーマットは極力小さいものを選択。メモリアクセス量はいつの時代もボトルネックの最右翼。
PlayerSettings（特にOtherSettings）やURPAssetといった常時影響する設定については、全ての項目の意味を把握しきるまでドキュメントを読み倒し、適切に設定出来るように。
（例えばVolumeUpdateModeはViaScriptingで運用できるように仕組み作りした方が良いですよ、みたいなー）

などなど...この手の一般的な最適化tipsは検索すれば一山いくらで見つかるでしょうからこのくらいにしておきます。


# 基本戦略：動的な制御が効く要素/効かない要素の分離

![_e9a3a01e-2208-47ff-81e3-f4e9452b81b3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/ada2d7db-a5a9-eff2-e8de-9e6d3711230a.jpeg)


　ここからがエントリの肝になります。**描画負荷には「制御が効く要素」と「制御が効かない要素」があってそれをちゃんと切り分けよう**、というお話をします。

### ピクセル負荷

　例えばキャラモデルや背景ののライティング/シェーディングが重たかったとします。このようなピクセルフィル負荷は、画面解像度を半分の面積にする、だけで単純に2倍のパフォーマンスを稼げます。
ポストプロセスが重たくても同様に画面解像度を下げるだけで軽くなりますし、もう少し真面目にやるなら縦横1/2サイズの縮小バッファを挟んでポストプロセスを処理しアップスケーリングで元の解像度に戻す、だけで単純計算で4倍弱のパフォーマンスを稼げるようになります。（まあイマドキは（そうでないと破綻するもの以外は）フル解像度で走らせるポストプロセスを探す方が難しいですが...）
あるいはアンチエイリアスがMSAAだった場合には、その倍率を下げるだけでも一気にフィル負荷を軽くすることが出来ます。

と、ピクセル負荷は基本的に「制御が効きやすい要素」である、といえます。

### 頂点負荷

 次に、キャラモデルの頂点数と同時表示数がハチャメチャに多いせいで重たかったとします。この場合、軽くするのは「表示数を減らす」か「LODを導入する」という選択肢が真っ先に挙がります。
前者「表示数を減らす」は、ゲームシステムに抵触する可能性が高く、プランナーやアーティストとの調整を迫られるでしょう。植生だったりプロップだったり、システムに影響しない要素も勿論ありますので、そちらはコントロール可能な要素にはなります。
後者「LODを導入する」は、アーティストの作業コストが一気に跳ね上がる上に、ストレージサイズ/ダウンロード時間/メモリ消費量などランタイムへの影響も大きいです。
Simplygon通すだけでその後の手作業不要でキャラモデルのLODが使える、くらいに進化した未来であれば話は別ですが、2023年現状ではまだまだ厳しいという認識です。

と、頂点負荷は基本的に「制御が効きにくい要素」である、といえます。

ちなみに、現状動いているゲームのボトルネックが頂点なのかピクセルなのかをざっくり判断する簡易的な方法は、画面解像度を1x1にしてゲームプレイを回すだけです。
これでフレームレートが上がるならピクセルネック、上がらなければ頂点ネックと判断して良いでしょう。
（各種RenderTextureのサイズが画面解像度にスケールされている前提です。他所のコードを拝見していると、意外と固定サイズで確保している例が散見されるので気をつけましょう）

### 制御が効かない要素、は早めにレギュレーションで縛る

　話を単純にするために頂点/ピクセルのみにフォーカスしましたが、あくまで例示としての切り分けと理解ください。

最低スペック端末をどこまで低くサポート出来るか、は、制御が効かない要素をどこまで削れるかに掛かっています。
どうやっても秒間60万頂点しか投入できない端末で60fpsを維持したいとなったら、1フレームに使える頂点数は1万、1000頂点のモデルを10体以上並べられません。
現実にはこんなはっきりとした数字が出せるわけではありませんが（そりゃ頂点アトリビュートのサイズだったり、総頂点数中のスキニングメッシュの割合だったり、変数はいくらでもあるわけで）、とはいえ、ある程度の概算値の算出は可能なはずです。

何が言いたいかというと、「**端末のスペック下限**」と「**最低保証フレームレート**」、そして「**画面内の総頂点数**(各オブジェクトの頂点数/総数、背景に割く平均頂点数、その他prop等)」のレギュレーションについて、**ゲーム開発の割と早い段階で決めておかないと、後々で地獄を見る可能性がありますよ**、ということです。

画面上にキャラが何体出るかまだ決まっていない？のであれば、ワーストケースを踏まえてキャラなり背景なりのメッシュ頂点数を算定しないとですね。既にモデル作り始めていて今更変えられない？では最低サポート端末はゲームシステムがある程度見えてきてから決めることになりますね。となってしまうわけです。

これが据え置き機向けゲームであれば、仮にマルチ展開だとしても最低スペックのハードウェア性能は明確なのでレギュレーションを切るのは難しくありません。足切り端末スペックをタイトルごとに設定できるモバイルゲームならではの悩みですね。

「適当に作っちゃって、動作に支障がなさそうなギリギリの端末を下限端末ってことにしとけばいいよ」みたいな富豪的ゲーム開発ももちろん自由ですし実際そういう開発現場が多いのかもという気はしますが、後々困るリスクを抱えていることは認識した方が良いのかな、と私は考えます。


### ピクセル側にもレギュレーションは必要

　頂点の話ばかりしてしまいましたが、ピクセル側も制御が効きやすいとはいえ下限スペック端末への考慮が必要です。

仮に画面解像度を1x1まで下げてゲームになるのであればよいのですが、もちろんそんなわけにもいかず、現実にはそこまで大きく解像度を減らすことは出来ません。もちろんFSR2.0やそれに類するような質の良いアップスケーリング技術を導入できれば画質を損なわずに更に解像度を下げることは出来ますが、低スペック端末の話をしているのにFSR2.0がーとか言われたら「ん？」って反応になりますよね。

ピクセルに強く影響を受けるレギュレーションとしては、VFXのオーバードロー率であったり、ポストプロセスの負荷や同時利用数だったりが挙げられます。
これに関しては、例えば`オーバードローカウンター`というツールをUnityEditor上に用意し、「現在のオーバードロー率をリアルタイムに数値で確認できて、レギュレーション違反な数値を超えたらアラートを投げる」仕組みで無理のあるピクセル負荷を回避しています。


### ピクセルフィル負荷を動的に制御する

　さてここからは、「**ピクセル負荷を動的に制御する方法**」について解説をします。
 これは低スペック端末への対応もありますが、一時的に負荷の高いシチュエーションが発生した際にもフレームレートを安定させること、を目的としています。もう一つ、端末負荷が高くなり発熱が起きた際にも活用されます。

まず真っ先に思い浮かぶのは画面解像度を調整することです。

画質設定の初期状態の時点で、低スペックな端末であれば極力解像度を下げ画質を犠牲に快適に遊べるようにします。逆にハイスペックな端末であれば余裕がある程度には解像度を上げたいです。

更にシーンの複雑さ/負荷の高さによって、画面解像度をゲーム中に動的に変えられる（**動的解像度、DynamicResolutionと呼びます**）ようにします。今からその実装方法について解説していきます。


# 動的解像度(DynamicResolution)を実現する

![_50cb282a-4934-4ac7-8eec-ecef7cdf80b7.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/ba79dd4c-63a7-58e9-5747-ee7e426d6c08.jpeg)

　まず前提条件として、DynamicResolutionで実現するべき目標は「**UIの解像度は元のまま、3Dの解像度のみ落とす**」だと私は考えます。UI解像度も含めてコロコロ変えるのはルック上厳しい上にパフォーマンス上のメリットも薄いので、あくまで3Dシーンのみの解像度を調整して負荷対策を行います。DynamicResolutionが実装されている据え置き機ゲームでも一般的にこのようになっているのではないでしょうか。

となると、画面解像度の変更と聞いてまず最初に思いつくであろう `Screen.SetResolution()` は、アプリ全体、つまりUIの解像度も一緒に変わってしまいますので不適格です。

目的である、動的な3D画面解像度の変更を達成するための手段として、Unity/URPでは「**UniversalRenderPipelineAsset.RenderScale**」と「**ScalableBufferManager.ResizeBuffers**」という2つの手段が提供されています。
このうちどちらかを選択しましょう。


### UI描画と3D描画を分離する

　とその前に、UI画面解像度を固定化する為の下準備が必要です。

UI描画をバックバッファへ直接ではなく、専用のRenderTextureに一度レンダリングを行い、フレーム最終段のオーバーレイCanvasにて3D描画結果であるバックバッファに対してUI+3Dのコンポジットを行うような仕組みを取り入れます。
これにより、バックバッファの解像度が変わっても（＝3D画面解像度を変えても）、UI画面解像度は一定に保つことが出来ます。

実はこの実装は「**3DはLinear色空間で、UIのみsRGB色空間で描画する**」仕組みとも共存しています。話の規模が大きくなりすぎるので詳細の説明は割愛しますが、このギミックについては将来的にQiitaに改めてまとめておきたいと考えています。が...いつになることでしょう...


### UniversalRenderPipelineAsset.RenderScale

　<A HREF="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@14.0/api/UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset.renderScale.html">`UniversalRenderPipelineAsset.RenderScale`</A> はURP標準で用意されているプロパティで、数値を変えるだけで簡単にバックバッファの解像度を変更することが出来ます。

ただし内部的には値を変更する度に、（ポストプロセス作業用RenderTextureなど関連する全ての）RenderTargetの開放＆再確保という非常に重たい処理が走りますので、連続して数値を書き換えるとCPUへの負担が激しくなり、何よりメモリ溢れの強いリスクがあります。

テンポラリRenderTexture管理機構（TemporaryRT）は使用していないRenderTextureが開放されるまで「15フレームの猶予期間」があるという話なので、RenderScaleの値を変更するのは**最低でも16フレーム以上の間を空ける**ようにしましょう。

ぶっちゃけた話をすると、そもそもの話としてこのRenderScaleはDynamicResolutionの実装を想定して作られているわけではないそうです。が、後述しますが現実的にはこれ以外に選択肢がないような状態になっています。

メモリ/CPUのパフォーマンス上の懸念を除けば、URPの根幹に存在しているので動作自体は安定しています。大いに活用するべきプロパティです。


### ScalableBufferManager.ResizeBuffers

　<A HREF="https://docs.unity3d.com/ScriptReference/ScalableBufferManager.ResizeBuffers.html">`ScalableBufferManager.ResizeBuffers`</A> / <A HREF="https://docs.unity3d.com/ja/2022.3/ScriptReference/Camera-allowDynamicResolution.html">`Camera.allowDynamicResolution`</A>は、UnityEngineがまさに動的な解像度変更の為に用意された機構です。

前述のRenderScaleとは違い、解像度を変更しても同じRenderTextureをそのまま利用し続け開放/再確保は行われないので、メモリ負荷への心配が少ないです（実際には解像度変更の度に管理オブジェクトの破棄/生成が行われるのでメモリ確保がゼロというわけではない、みたいですが、それでもRenderScaleのそれよりは圧倒的に身軽なはずです）。おそらくはCPU負荷も（それなりに重たい、と釘を刺されてはいますが）RenderScaleのそれよりは軽いと想定されます。

また制限として **「縦横の比率は固定」「解像度変更は5%刻み限定」「解像度の縮小は最小で25%まで」** があります。とはいえ、これらは特に問題とはならないでしょう。

ここまでは良いことづくめで「Unity直々に用意された機能なんだからRenderScaleを使うよりこちらの方がいいじゃん」となりそうなものですが、残念ながらこのResizeBuffersには致命的な問題が2つあります。

まず1点目、**OpenGLES3では非対応**なこと。Android環境での描画エンジンはおおよそVulkanかOpenGLES3かの2択となっており、パフォーマンスや機能の多さでいうとVulkanの方が間違いなく優位なのですが、（少なくとも2023年の現状においては）**VulkanはAndroid実機上でのクラッシュ率が有意に高い**という数値が各種クラッシュレポートから証明されてしまっています。

将来的にUnityなりVulkan/ドライバ側なりのアップデートで改善されるのかは分かりませんが、少なくともいまこの瞬間では、Vulkanを選択すること自体が考慮外となっています。ですので、Android環境ではResizeBuffersは使えずRenderScaleしか選択肢がない状態になっています。

そして2点目、この**ResizeBuffersはUnity社によってまともにテストされておらず**、URPのバージョンによってはまともに動かないことが多々あるということです。

具体例を挙げると、URP14.0.5ではなぜか `camera.allowDynamicResolution = false;` とURP内部コードにて書かれていて強制的にどうやってもResizeBuffersを利用することが出来なくなっていました。更にURP14.0.7ではこの行は消えていて利用自体は可能になっていたのですが、肝心の絵が壊れるので結局使えない...という、どうしようもない状態になっていました。

要は、QAとしてこのResizeBuffersの動作テストが含まれていないのですね。

現在の最新バージョンでは流石に治っているかと思いますが、まともにテストされたAPIではないことは間違いなく、使うこと自体にリスクがあります。~~UnityのAPIでマイナーなものはだいたいこの問題をはらんでいるわけですけれども...いや、このResizeBuffersはそこまでマイナーなAPIではないはずですが...Unityというエンジンがそういうものだと割り切~~

以上の2点から、私はRenderScaleを使ってDynamicResolutionを実装しています。


### フレームレート低下/発熱を検知して画面解像度を制御する

　さて、動的な画面解像度の変更、DynamicResolutionが実現できるようになったら、いよいよピクセルフィル負荷の自動制御に入ることが出来ます。
とはいっても、実装自体は難しいことは特にありません。

- **明らかに処理が追いついておらずフレームレートが落ち込んでいる、あるいは端末の発熱が検知されたら3D画面解像度を一気に落とす**
- **逆にフレームレートや発熱が安定しており、3D画面解像度が落ちている状態であれば、少しずつ戻していく**

基本的にはこれだけです。

解像度の変更は前述の通り重たい処理であることとメモリ負荷の問題があるので、ある程度のフレーム間隔を確実に空けるようにします。細かくやりすぎると無駄に重たくなり本末転倒になってしまいます。

フレームレート低下は今すぐ改善したいので、検知したら大きく解像度を落とします。逆に解像度を元に戻すのは、慎重にゆっくり上げていきます。でないとまたすぐフレームレート低下を招く危険性があります。

なお、何かしらの理由でスパイク（一瞬だけの処理落ち）が起きたと判断されるフレームはフレームレート低下の判断には含めないようにしておきます。


### 処理落ちの理由は、GPUなのかそれともCPUなのか？

　この仕組み上、大きな問題となるのは「**処理落ちが、ピクセルフィル負荷ではない理由であった場合**」です。この考慮をせずにいると、例えばCPU側のAI計算で処理落ちしてました、という状況であっても、フレームレートの低下を検知して無駄に画面解像度を下げて負荷軽減を行おうとしてしまいます。「重たい原因がGPU負荷である」ことを正しく判別しなければなりません。

このために使えそうな数値が `FrameTimingManager.cpuFrameTime` と `FrameTimingManager.gpuFrameTime` の比較...なのですが、後者は端末によっては0や狂った値が返ってくるので残念ながら利用できません（正しく値が拾える端末でのプロファイリングには使えるのですが、ランタイムでの負荷制御に使うことはご法度です）。

仕方なくワークアラウンド的な逃げ道として、`FrameTimingManager.cpuFrameTime` と `FrameTimingManager.cpuMainThreadFrameTime` の比較を用いて判定を行います。これであれば、精度こそ微妙ですが、CPUが埋まっているせいで処理落ちしたのかな？と類推することが可能です。

そもそもですが、FrameTimingManagerは数フレーム前の情報を拾っているらしく、どう頑張ったところで正確な判断材料にはなりません。
この辺り、ハードウェアがある程度固定されており欲しい値を安定して拾える据え置き機と比べ、千差万別な端末が存在し挙動が安定しないモバイルの弱点が出てしまいますね。多少の妥協は仕方がありません。


# 画質オプション設定の自動調整機能

![_7f988a59-c774-4b40-866c-846e2de8dd66.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/0a6978a4-585e-4314-81d5-68f3b17767b8.jpeg)

　DynamicResolutionの導入で、一時的に重たいシチュエーションが発生した際によるフレームレート低下や端末発熱時の「ある程度の」改善を行うことは可能になります。

しかし例えば、低スペックな端末で分不相応な高画質のオプション設定を行ってしまっていた場合、どんなに3D画面解像度を落としてもなおパフォーマンスが足りずガックガクでまともなゲームプレイが不可能、などという状況は簡単に起こり得ます。
DynamicResolutionだけでは負荷制御として足りていない可能性がある、ということですね。

この対策の為、**DynamicResolutionで最低解像度に張り付いたまま＝パフォーマンスが明確に足りていない状況が続いた際には、画質オプションで設定された項目をその場で自動的に下げる**という対応を行っています。

凡例としては以下の通りです。まあいまどきのゲームならだいたい同じような設定項目を持っているかと思います。

| オプション | 設定 |
|:-----------|------------:|
| 基準FPS | 60over -> 60 -> 60to30 -> 30 |
| アンチエイリアス(MSAA) | x8 -> x4 -> x2 -> AAなし |
| 影 | 高品質 -> 標準品質(カスケードなし) -> 影なし |
| 異方性フィルタ | 最大値 -> 通常 -> フィルタなし |
| 視覚効果 | 高品質 -> 標準品質 -> 低品質 |

※ 60to30 : ゲーム処理(CPU)を60fps,描画(GPU)を30fpsで動かすモード

どの画質を優先的に下げるかはアーティストとエンジニアで協議/判断し、見た目にインパクトが低いものから順繰りに行うようになっています。

そして、この自動的な画質設定変更は一度下がったら（画面解像度とは違い）そのインゲーム中は元に戻しません。この画質設定の切り替えは絵へのインパクトが大きく、コロコロ変わっていたら問題であろう、また動作不安定の原因にもなりそう、という判断です。

インゲームを抜けアウトゲームに戻った時点で下がった設定はリセットされます。次のインゲーム開始時にはまた重たい状態から始まってしまいますが、すぐ適正な画質設定に落ち着きます。
本来はこれが何度も繰り返された時点でユーザーが指定した画質設定そのものを書き換えた方が良いのかもしれませんが、その辺りはまだ未実装になっています。

ちなみに、DynamicResolutionも画質オプションの自動調整機能も、どちらもゲーム初回起動時からデフォルトで動作するようになっていますが、オプションでDisableにも出来るようになっています。どんなにフレームレートが落ちて端末がアチアチになってでも画質を絶対に下げたくない人、が何人いるのか正直分かりませんが、そういう需要がある、と言われたので一応用意しています。


# 初回起動時の画質設定、どうやって決めるの問題

![_9ceb40d6-6f56-49c9-9475-a45609ab233b.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/6cc33827-89c0-069f-35d8-3d999c581619.jpeg)

　ゲームの初回起動時、その端末の性能に合わせた適切な画質設定を自動的に割り当てたいですよね。折角の最新型高級スマホなのに低画質な設定が割り振られてしまったらゲームの魅力を損なうことになりますし、逆に、スペックに似合わない高画質設定が割り振られてしまったら目も当てられない事態が待っています。

初回起動時にユーザーに画質設定の大まかなプリセットから選ばせる、というのが割とよく見かけるパターンかと思いますが、ゴリゴリのPCゲーマーならいざ知らず、スマホのユーザーに自身の端末のスペックを正しく認識しろ、というのは少々無茶な要望かなと私は考えます。できれば自動的に適切な画質設定が割り振られて欲しいのです。

iOSに限定すれば、端末の絶対数が少ないのでそれぞれの `SystemInfo.deviceModel` 毎に決め打ちしてしまってもそこまでの手間ではありません。いま現在存在していない将来のモデルについても、スペックは上がりこそすれ下がることはまずあり得ないので、その意味でも対応は難しくありません。

問題はAndroid端末です。~~本当に開発者サイドからするとAndroidは辛い事が多い...~~
文字通り無限に端末のバリエーションが存在するので何らかの手段でスペックの推定を行う仕組みが必要となります。

この端末スペックの推定方法の実装について、いくつかの選択肢を考えています。


### 1. SystemInfoから拾えるスペック情報から類推する

　実装はとても簡単ですが、ぶっちゃけた話、`SystemInfo`からは碌な情報が得られません。
 端末スペック推定に役立ちそうなプロパティは以下の3つくらいでしょうか。

- コア数：`processorCount`
- CPUクロック数：`processorFrequency`
- メインメモリ搭載量：`systemMemorySize`

さて...ここで拾える数値、実は致命的な問題を抱えています。
**詐欺申告してくる端末がたまに存在しているのです**。

例えば`"Google Pixel5"`はSoCがSnapdragon 765Gで、8コアCPUの 1.8GHz x 6 + 2.4GHz + 2.2GHz という作りになっているのですが、`processorFrequency`はなんと`4800`、4.8GHzであると返してきます。どこから出てきた数字だ！？
`"samsung SC-52B"`という端末でも、Snapdragon 888 の 2.8GHz + 1.8GHz というCPU構成なのにもかかわらず、`processorFrequency`は`5683`という無茶苦茶な数値を返してきます。

この詐欺申告さえなければ、上記の3つを用いて「正しくは全くないけど、おおよそのスペック高低具合程度なら類推できるかなあ...」と楽観的に考えていたのですが、残酷な現実を突き付けられました。

現在は工数的な問題でこの手法を採用していますが、はっきり言って**まともに動いていない**ので、今後の改善は必須だと考えています。


### 2. deviceModelと端末スペックの組み合わせを事前にデータベース化

　ランタイム側の実装は至極単純ですが、組み合わせのデータベース化、がとにかく手作業でやるには厳しいです。リリース後も端末は増えていくので定期的なメンテナンスが必須となり、タイトル運営の負の遺産が一つ増えます。
Android端末は本当に星の数ほど種類が存在し全てを網羅するのは不可能なので、確実に漏れも発生します。この場合、1.の`SystemInfo`からの類推も一緒に併用する必要が出てくるでしょう。

あるいは、端末毎のAntutuの結果をリストアップしてくれている各種サイトから情報を~~吸い出して~~使わせて頂いて、上手いことマッチングさせる、とか...？
何にせよ、泥臭くて個人的には近寄りたくない作業が待っています。

これはただの妄想ですが、**端末のDeviceNameごとに「画質オプション設定の自動調整機能」を用いて最終的に落ち着いた「適切と想定される画質オプション」をサーバに収集して、新規ユーザーの初期状態での画質設定を自動的にそれに合わせる**、みたいな芸当ができたらいいな、とか考えているのですが...
正直なところその工数とコスパが見合うかと言われると...まあ、将来、余裕が出来たら試してみたいですね...（目を泳がせながら）


### 3. 初回起動時にベンチマークを走らせてその場で判定

　~~端末のパフォーマンスを把握出来るベンチマークを用意して、それを初回起動時に走らせて適切な画質設定を選択します。精度はそこそこで良いので、数秒程度で終わることが望ましいです。~~

~~工数の問題でまだ試してはいないのですが、手間と精度のバランス的に悪くないんじゃないかなー？と夢想しています。実は既に実装されているタイトルとかあったりするんでしょうかね...？~~

~~懸念点としては、ベンチマークが走る時間分だけ遊べるようになるまでのラグが発生するので、それがユーザー離脱率の上昇に繋がらないか？という部分ですね。~~

実装しました。割とうまくいきそうです。いずれ新しいエントリとして書きます。書かないかもしれません。


# 他にも色々

### ShaderVariantCollection.WarmUpによるシェーダの事前ロード

　本来的なメリットであるシェーダ初回ロード時のスパイク対策として以外にも、例えばインゲーム開始時のロード時間のうちの幾ばくかを占める大量のシェーダロードに掛かる時間を裏で先読みし隠蔽できるのがとても大きいです。低スペックな端末ほどシェーダロードに掛かる時間は増えるので、下限スペックが低いタイトルほど有効です。
 
何はなくとも、ShaderVariantCollectionの導入は必須である、と認識しましょう。

ただしPBRなシェーダなど、サイズ大きめのシェーダ複数を同じShaderVariantCollectionに固めて一気にWarmUpしてしまうと、ANRに引っかかるレベルのスパイクが発生する程に重たくなってしまいます。`WarmUpProgressively()`を使って、企業ロゴやタイトル画面の表示中に毎フレーム少しずつ進めるのが良いでしょう。

`WarmUpProgressively()`が存在しない古いUnity（古いといっても初出がUnity2022.2なのでつい最近追加されたAPIです）を使っている場合は、ShaderVariantCollectionを細切れ大量に用意して、毎フレームちまちま`WarmUp()`を呼び出す必要があります。なんともウンザリするような話ですが、他に選択肢がないので我慢するしかありません。

# さいごに

![_e090a06f-1c34-4bea-90b1-bbb8a75db031.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/cb1480e5-5b2e-d8aa-f580-3dc8b6a20997.jpeg)

　モバイル端末のスペック差は尋常でなく広がってきており、そこを主戦場とするゲーム開発ではどうしても対応に追われるかと思います。
描画周りの画質を、端末スペックやシーンの複雑性に合わせての自動調整を実現することによって、アセット製作コストを増やさずに性能差を埋めることができて色々助かるよね、というのが今回の主題でした。

ちなみにこのエントリを書く切欠になったのは、前述したUnity推奨のDynamicResolution機能がバグっていることをUnity社に確認した際、「他社モバイルゲームはどうやってDynamicResolutionを実現してるんですか？」と質問したところ「動的解像度まで踏み込んでいるケースはほとんどないです」と返答を頂いたからなんですよね。
据え置き機ではDynamicResolutionなんて一般的な技術ですが、むしろモバイル環境でこそ必要じゃないのかなあ...と衝撃を受けたんですよね。もう1年半前のやり取りなので、いまはまた状況が違っているのかもしれませんが。

何にせよ、参考になれば幸いです。

しかし、なんでいつもこんなに長くなってしまうんだろう...これでも言葉が足りておらず補足したい部分が山ほどあって、でも文章が枝葉生えすぎで読みにくくなりすぎる（現状でも既に厳しいけど）から削っているのに...
もう少し書きたいことを絞った上で、定期的にアウトプットするのが正しい姿なんだろうなあ...とは理解しつつ、じゃあ実践できるかというと...（今回もアドカレの締切があったから書き切ったものの、ずっと昔からまとめておきたいなと思っていたのに手が動かず...）

と、自分の筆不精っぷりを嘆きつつ終わりたいと思います:skull:

それではまた来年の12/24にお会いできたらいいですね！
　
 
　
 ![wXXoUrm.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/518432/4ed56fa7-e6a9-f186-a518-3b6d817f6332.gif)