URP環境で3Dの描画解像度だけ下げる（UIはそのまま）

# 前書き

この記事は、2023のUnityアドカレの12/15の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい！

https://qiita.com/advent-calendar/2023/present-calendar#qiita_award

# はじめに

描画負荷を抑えるための方法として、「解像度を下げる」というのはとても効果があります。一方で、解像度を下げている訳ですから、プレイヤーの体験に影響が出ると。しかし、UIだけ高解像度にしていれば、そのほか（ここでは便宜上3Dと呼びます）は下げてしまっても意外と気にならないといわれています。

ということで、特にモバイルでの開発では、UIと3Dの解像度を分けようということがしばしばあります。それを実現するためのいくつかの方法をまとめ比較してご紹介します。

# URPのRenderScaleとScreenOverlayMode使う（オススメ度 ☆☆/難易度 ☆）

カメラは一つだけ、UICameraは使用しません。

URPには、RenderScaleという描画解像度をスケールする機能があります。これは、スクリーンバッファ（OSのWindowやActivityが持っているバッファ）のサイズを変えず、一度スケールしたオフスクリーンバッファへ書き込み、その後スクリーンバッファにコピーします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/3bc69b38-fa93-7848-0c5a-48895ecd4c90.png)

一方で、CanvasのScreenOverlayModeは、スクリーンバッファへのコピーの後（URPの描画パイプラインの外）で描画実行されます。ですから、スクリーンバッファの解像度で描画されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/1564f3b6-3701-0081-4afe-dc8d2984d249.png)

UIの解像度（スクリーンバッファのサイズ）は、`Screen.SetResolution`で設定し、URPのRenderScaleを調節することで所望の解像度にするということです。

### 問題点

UIがURPの描画パイプラインの外で書かれてしまうため、ポストエフェクトなどを差し込む余地がありません。仮に差し込めたとしても、スクリーンバッファはWriteOnly（TextureとしてBindできない）ので実用的ではないでしょう。

また、ScreenOverlayModeは、シーンビュー上で、ワールド=スクリーン座標として配置されるため、とても邪魔です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/c5490ee9-6b54-1769-caa0-984c148cf78a.png)

# RenderTextureを使う（オススメ度 ☆☆☆/難易度 ☆）

3Dを一旦RenderTextureに書いて、UI上にRawImageで置きます。3Dの描画解像度はRenderTextureのサイズまで縮むことになります。CanvasのRenderModeはUICameraに設定することができます。ポストエフェクトもかけられますし、巨大化も避けられます。（カメラの近くにものを置かないなら、3DCameraを設定してもよいでしょう）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/8801710b-bc29-49da-d789-f1fe8bc2d0b6.png)

この方法は（解像度決め打ちなら）ノーコードで実現することすらできます。

### 問題点

ある意味良い点でもあるのですが、Canvasを編集しているときに、Canvasの上にフルスクリーンのRawImageが乗ってしまいます。邪魔です。

解像度の変動などに合わせてRenderTextureのリサイズも必要です。また、RenderTextureへの参照をUIカメラと3Dカメラでそれぞれ握らす必要があります。

# カメラスタッキング（オススメ度 ☆/難易度 ☆☆）

URPのカメラスタックは、BaseCameraにOverlayカメラをリスト的に追加できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/1bb40866-05f0-790f-9550-990dbf00cb7e.png)

カメラスタックは同じRenderTargetへ順番に描画していき、最後のCameraでスクリーンバッファにFinalBlit（ポストエフェクトがある場合は合体）します。ポストプロセスはそれぞれで書けることができます。

### 問題点

そもそも同じフレームバッファへ描画してしまうので、解像度に差がつけられません。BaseCameraの前にRenderTargetを小さいフレームバッファに切り替えて、BaseCameraが終わったらもともとのRenderTargetに転写（アップスケール）してRenderTargetを再設定すれば良さそうですが、URPの仕組み上この処理を差し込む余地はありません。（RenderFeatureでも無理）
URPそのものの改造必須です。

また、ポストエフェクトはカメラごとにかけられますが、AntiAliasingは最後のFinalBlitの時にしかされません。

# 完全に独立した2つのカメラ（オススメ度 ☆☆/難易度 ☆☆☆）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/a7e091a7-17ca-11cd-cd9d-a72327834a7e.png)

双方のカメラそれぞれ別のスタックにする方法です。
UICameraのPriorityを上げて、Clearしないように

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/5fc0f833-2f15-4e5f-6d11-c6c7b3ecf08e.png)します。

1. 3DCameraで小さめのフレームバッファに描画
1. アップスケールしてスクリーンバッファにFinalBlit
1. UICameraがスクリーンバッファに直書き（Clearしない）

こうすれば、良さそうですが…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/c7baa99b-c14e-4e93-6746-101ff9b12764.png)

3DCameraが映りません！

「UICameraがスクリーンバッファに直書き」を実行させるには、ポストエフェクト関連をすべてOffにし、URP設定のRenderScaleを1にし、UR設定のIntermediateTextureをAutoにする必要があります。IntermediateTextureを使うというのはスクリーンバッファとは別にフレームバッファを用意し、そちらへ一旦描画した後にスクリーンバッファへ書き戻すことです。カメラスタッキングのところで説明したことですね。逆に条件を達成した場合にはIntermediateTextureがOffになるということです。

IntermediateTexture（別フレームバッファ）を使うとなぜ映らないのかということですが、FinalBlit（スクリーンバッファへの書き戻し）の時のBlitがアルファブレンドになっていないからです。

そこまですると、上記の目論見どおり行きます。

### 問題点

RenderScaleが1に固定されてしまいますし、そもそもRenderScaleをスタック個別に設定できないので、解像度に差がつけられません。やはり、URPの改造が必要です…。

やり方としては、まず、スタックごと（カメラごと）にRenderScaleをいじれるように、`UniversalRenderer.Setup`のはじめ当たりのタイミングで、`CameraData.renderScale`を変更できるコールバックの口を用意します。ここには元々URP設定のrenderScaleからコピーされるようになっています。

そして、IntermediateTextureが必要（UIにもポストエフェクトをかけたりする）なら、FinalBlitの部分に手を加える必要があります。

# まとめ

UIと3Dの解像度に差をつけるというのは、必ずといっていいほど実装するべき機能なのですが、なぜかデフォルトでスマートにやる方法が見つかりません。いつも悩み悩みやっています…ここでもいくつか挙げましたが、正直どれが一番マシなのかも微妙ですよね🤮