📸

UnityでRenderTextureとCulling Maskを使ってレンダリングを高速化する

2023/03/27に公開

はじめに

Unityでのレンダリング速度を向上させるために、RenderTextureとCulling Maskを活用する方法を紹介します。更新頻度の低い描画内容をRenderTextureで保持し、Culling Maskで不要な描画を非表示にすることで、レンダリングを高速化することができます。

I will show you how to take advantage of RenderTexture and Culling Mask to improve rendering speed in Unity. You can speed up rendering by using RenderTexture to hold drawing content that is rarely updated, and Culling Mask to hide unnecessary drawing.

RenderTextureとは

RenderTextureはテクスチャの一種で、Cameraで描画された内容を一時的に保持できます。また、保持した内容はRawImageやPlaneなどにそのまま表示できます。

この機能を使うことで、背景や影などの更新頻度が低いオブジェクトをRenderTextureに描画しておき、そのあとはCameraの描画を停止してRenderTextureを表示し続けることでレンダリングを高速化できます。

今回の記事の概要
今回の記事の概要

多くのSetPassCallやDrawCallをRenderTextureの描画1回に変換できるため、SpriteAtlasなどを使用しても最適化に限界がある状況などでは有効な手段です。一方、こういった仕組みを入れてしまうと保守性は下がってしまい機能修正などが大変になるので注意してください。

Cameraの描画をRenderTextureに出力する方法

Cameraでは2種類の方法で出力先のRenderTextureを設定できます。

シーンから設定する方法

1つめはシーンから設定する方法です。この場合は、あらかじめAssetとしてRenderTextureを作成しておく必要があります。

シーンからRenderTextureを設定する方法
シーンからRenderTextureを設定する方法

Scriptから設定する方法

2つめはScriptで設定する方法です。RenderTextureはSerializedFieldとして作成済みのものを渡しても良いですし、新たにScriptで作成することもできます。

mainCamera.targetTexture = new RenderTexture(Screen.width, Screen.height, 0);
mainCamera.Render();

どちらの方法でも問題ありませんが、今回はgistにまとめて処理を貼りたいので全てScriptで行います。

出力されたRenderTextureをPlaneに表示する方法

出力されたRenderTextureをPlaneに表示します。今回はShader、Material、Planeを全てScriptで作成しています。

var renderTexture = new RenderTexture(Screen.width, Screen.height, 0);
var unlitMaterial = new Material(Shader.Find("Unlit/Texture"));
unlitMaterial.SetTexture("_MainTex", renderTexture);

var plane = GameObject.CreatePrimitive(PrimitiveType.Plane);
// カメラの目の前にPlaneを表示
plane.transform.eulerAngles = new Vector3(90f, 180f, 0f);

var planeRenderer = plane.GetComponent<Renderer>();
planeRenderer.sharedMaterial = unlitMaterial;
planeRenderer.sortingOrder = targetSortingOrder;

表示方法はPlaneがおすすめです。World座標に表示できるうえ、SortingOrderによって表示順を制御しやすくなります。

Culling Maskで対象オブジェクトの描画を高速にオンオフする

さて、MainCameraにRenderTextureを設定しましたが、このままではRenderTextureに含めたくないオブジェクトまで同時に描画されてしまいます。

そこで、Culling Maskという機能を使用して描画対象を切り替えます。Culling Maskを使うことで、GameObjectに設定されたLayerを元にして、そのLayerを描画するかを高速に切り替えられます。

Culling Maskの設定欄
Culling Maskの設定欄(チェックを入れると描画される)

Layer設定欄
Layer設定欄

GameObjectのLayer設定欄
GameObjectのLayer設定欄

Culling Maskの詳しい使い方はググるなりChatGPTに聞いてもらった方が分かりやすいかと思うので省略します。今回は更新頻度が低いGameObjectのLayerを”Heavy"レイヤーとし、このレイヤーだけをRenderTextureへ描画します。

コード

これら全てをまとめたコードは次のようになります。任意のオブジェクトに付与して、SerializedFieldに必要な情報を設定してから実行すると、この記事に書かれた処理が実行されます。Rキーを押すごとに画面が更新されます。

コードの動作結果

では実際にコードを動かしてみます。

まずは更新頻度が低く重い背景の例として、今回はSpriteRendererが数万個表示されている重い画面を作りました。(実際にはこんな画面を作ってはいけません)

重い画面をそのまま表示したときのProfiler
重い画面をそのまま表示したときのProfiler

バッチングされていてDrawCallsは9ですが、30〜40fpsほどになっています。

次に、今回のコードでRenderTextureを表示し、Culling MaskのHeavyレイヤーをオフにして全てのSpriteRendererを非表示にします。

RenderTextureで表示したときのProfiler
RenderTextureで表示したときのProfiler

見た目は同じですが、Profilerを見ると4000fpsになっていて、処理時間はほとんどかかっていません。Rキーを押して何度か再描画してみます。

RenderTextureを再描画したときのProfiler
RenderTextureを再描画したときのProfiler

押した瞬間だけ元のfpsに戻っていますが、ずっと重いよりはマシな性能になっていると思います。あまりに更新頻度が高い場合はRenderTextureにする意味がないので、DOTSなど別の方法で最適化することを検討しましょう。

補足:RenderTextureのメモリ使用量について

RenderTextureのサイズは、1920x1080で表示した場合は約8MBです。問題ないサイズだと思いますが、気になるようであればRenderTextureの解像度をnew RenderTexture(Screen.width / 2, Screen.height / 2, 0)のように調整することで軽量化できます。縦横をそれぞれ半分にするとサイズは4分の1になり約2MBです。許容できる範囲で解像度を落とすと良いでしょう。

応用

Orthographicで2D表示する場合に限られますが、さらに描画回数を減らすアイデアを紹介します。

現在のコードではMainCameraが少しでも動くとRenderTextureの描画範囲外が表示されてしまい、再描画が必要になります。そこで再描画を防ぐために、RenderTextureに出力する時だけorthographicSizeを1.5倍などにすることで、MainCameraよりも広い範囲をあらかじめ描画しておくことができます。すると、MainCameraが少し動いてもRendererTextureの範囲内に収まるので、一定距離を動くまでは再描画が不要になります。

例えば2Dゲームでキャラクターを追従するようにカメラが動き、一定距離動いたらリッチな背景や影などを再描画したい場合などはこの応用が役立つと思います。

まとめ

この記事では、Unityでのレンダリング速度を向上させるためにRenderTextureとCulling Maskを活用する方法を紹介しました。

更新頻度が低い描画内容をRenderTextureに保持し、Culling Mask機能を使用して不要なオブジェクトを非表示にすることでレンダリングを高速化できます。

保守性は下がるので他の最適化手法がある場合はまずはそちらを試すべきですが、比較的簡単に実装できる回避策として有効な手段かと思います。

Discussion