本章では、Unityのグラフィックス機能周辺のチューニング手法について紹介します。
レンダリングパイプラインの中でもフラグメントシェーダーのコストはレンダリングする解像度に比例して増加します。とくに昨今のモバイルデバイスはディスプレイの解像度が高く、描画解像度を適切な値に調整する必要があります。
モバイルプラットフォームのPlayer Settingsの解像度関連の項目に含まれているResolution Scaling ModeをFixed DPIに設定すると、特定のDPI(dots per inch)をターゲットに解像度を落とすことができます。
図7.1: Resolution Scaling Mode
最終的な描画解像度は、Target DPIの値とQuality Settingsに含まれるResolution Scaling DPI Scale Factorの値が乗算されて決定します。
図7.2: Resolution Scaling DPI Scale Factor
スクリプトから描画解像度を動的に変更するには、Screen.SetResolutionを呼び出します。
現在の解像度はScreen.widthやScreen.heightで取得することができ、DPIはScreen.dpiで取得できます。
リスト7.1: Screen.SetResolution
1: public void SetupResolution()
2: {
3: var factor = 0.8f;
4:
5: // Screen.width, Screen.heightで現在の解像度を取得
6: var width = (int)(Screen.width * factor);
7: var height = (int)(Screen.height * factor);
8:
9: // 解像度を設定
10: Screen.SetResolution(width, height, true);
11: }
Screen.SetResolutionでの解像度設定は実機でのみ反映されます。
Editorでは変更が反映されないため注意しましょう。
半透明マテリアルの使用はオーバードローの増加の大きな原因となります。オーバードローとは画面の1ピクセルに対して複数回フラグメントの描画が行われることで、フラグメントシェーダーの負荷に比例してパフォーマンスに影響を与えます。
とくにParticle Systemなどで大量に半透明のパーティクルを発生させる場合などにオーバードローが大量に発生することが多いです。
オーバードローによる描画負荷の増加を軽減するには、以下のような方法が考えられます。
Built-in Render PipelineのEditorではSceneビューのモードをOverdrawに変更することでオーバードローを可視化することができ、オーバードローの調整の基準として有用です。
図7.3: Overdrawモード
Universal Render Pipelineでは、Unity 2021.2から実装されているScene Debug View Modesによってオーバードローを可視化できます。
ドローコールの増加はCPU負荷にしばしば影響を与えますが、Unityにはドローコールの数を削減するための機能がいくつか存在します。
動的バッチングは、動的なオブジェクトをランタイムでバッチングするための機能です。この機能を使用することで、同じマテリアルを使用している動的なオブジェクトのドローコールを統合して削減できます。
使用するには、Player SettingsからDynamic Batchingの項目を有効にします。
また、Universal Render PipelineではUniversal Render Pipeline Asset内のDynamic Batchingの項目を有効にする必要があります。ただUniversal Render PipelineではDynamic Batchingの使用は非推奨となっています。
図7.4: Dynamic Batchingの設定
動的バッチングはCPUコストを使用する処理であるため、オブジェクトに適用させるには多くの条件をクリアする必要があります。以下に主な条件を記載します。
動的バッチングはCPUの定常負荷に影響を与えるため、推奨されない場合があります。後述するSRP Batcherを使用すると動的バッチングに近い効果を得ることができます。
静的バッチングは、シーン内で動かないオブジェクトをバッチングするための機能です。この機能を使用することで、同一マテリアルを使用している静的なオブジェクトのドローコールを削減できます。
動的バッチングと同様にPlayer SettingsからStatic Batchingを有効にすることで使用できます。
図7.5: Static Batchingの設定
オブジェクトを静的バッチングの対象とするには、オブジェクトのstaticフラグを有効にする必要があります。具体的にはstaticフラグの中のBatching Staticというサブフラグを有効にする必要があります。
図7.6: Batching Static
静的バッチングは動的バッチングとは違いランタイムでの頂点の変換処理を行わないため低負荷で実行できます。しかし、バッチ処理によって結合されたメッシュ情報を保存するためメモリを多く消費することに注意が必要です。
GPUインスタンシングとは同一メッシュ・同一マテリアルのオブジェクトを効率的に描画するための機能です。草や木のように同じメッシュを複数回描画する場合のドローコールの削減が期待できます。
GPUインスタンシングを使用するには、マテリアルのInspectorからEnable Instancingを有効にします。
図7.7: Enable Instancing
GPUインスタンシングを使用できるシェーダーを作成するためにはいくつか専用の対応が必要となります。以下にBuilt-in Render PipelineでGPUインスタンシングを使用するための最低限の実装を行ったシェーダーコードの例を記載します。
リスト7.2: GPUインスタンシングに対応したシェーダー
1: Shader "SimpleInstancing"
2: {
3: Properties
4: {
5: _Color ("Color", Color) = (1, 1, 1, 1)
6: }
7:
8: CGINCLUDE
9:
10: #include "UnityCG.cginc"
11:
12: struct appdata
13: {
14: float4 vertex : POSITION;
15: UNITY_VERTEX_INPUT_INSTANCE_ID
16: };
17:
18: struct v2f
19: {
20: float4 vertex : SV_POSITION;
21: // フラグメントシェーダーでINSTANCED_PROPにアクセスするときのみ必要
22: UNITY_VERTEX_INPUT_INSTANCE_ID
23: };
24:
25: UNITY_INSTANCING_BUFFER_START(Props)
26: UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
27: UNITY_INSTANCING_BUFFER_END(Props)
28:
29: v2f vert(appdata v)
30: {
31: v2f o;
32:
33: UNITY_SETUP_INSTANCE_ID(v);
34:
35: // フラグメントシェーダーでINSTANCED_PROPにアクセスするときのみ必要
36: UNITY_TRANSFER_INSTANCE_ID(v, o);
37:
38: o.vertex = UnityObjectToClipPos(v.vertex);
39: return o;
40: }
41:
42: fixed4 frag(v2f i) : SV_Target
43: {
44: // フラグメントシェーダーでINSTANCED_PROPにアクセスするときのみ必要
45: UNITY_SETUP_INSTANCE_ID(i);
46:
47: float4 color = UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
48: return color;
49: }
50:
51: ENDCG
52:
53: SubShader
54: {
55: Tags { "RenderType"="Opaque" }
56: LOD 100
57:
58: Pass
59: {
60: CGPROGRAM
61: #pragma vertex vert
62: #pragma fragment frag
63: #pragma multi_compile_instancing
64: ENDCG
65: }
66: }
67: }
GPUインスタンシングは同一のマテリアルを参照しているオブジェクトにのみ作用しますが、各インスタンスごとのプロパティを設定できます。上記のシェーダーコードのように対象のプロパティをUNITY_INSTANCING_BUFFER_START(Props)とUNITY_INSTANCING_BUFFER_END(Props)で囲むことで個別に変更するプロパティとして設定できます。
このプロパティをC#でMaterialPropertyBlockのAPIを使用して変更することで個別のカラーなどのプロパティを設定できます。ただ、あまりに大量のインスタンスに対してMaterialPropertyBlockを使用すると、MaterialPropertyBlockでのアクセスがCPUのパフォーマンスに影響を与えることがあるので気をつけましょう。
SRP Batcherとは、Scriptable Render Pipelineでのみ使用できる描画のCPUコストを削減するための機能です。この機能を使用することで、同一のシェーダーバリアントを使用する複数のシェーダーのセットパスコールをまとめて処理することができるようになります。
SRP Batcherを使用するには、Scriptable Render Pipeline AssetのInspectorからSRP Batcherの項目を有効にします。
図7.8: SRP Batcherの有効化
また、ランタイムでは以下のC#コードでSRP Batcherの有効・無効を変更できます。
リスト7.3: SRP Batcherの有効化
1: GraphicsSettings.useScriptableRenderPipelineBatching = true;
シェーダーをSRP Batcherに対応させるには以下の2点の条件をクリアする必要があります。
UnityPerDrawに関しては、Universal Render Pipelineなどのシェーダーでは基本的にデフォルトで対応されていますが、UnityPerMaterialのCBUFFERの設定は自分で行う必要があります。
以下のように、マテリアルごとのプロパティをCBUFFER_START(UnityPerMaterial)とCBUFFER_ENDで囲みます。
リスト7.4: UnityPerMaterial
1: Properties
2: {
3: _Color1 ("Color 1", Color) = (1,1,1,1)
4: _Color2 ("Color 2", Color) = (1,1,1,1)
5: }
6:
7: CBUFFER_START(UnityPerMaterial)
8:
9: float4 _Color1;
10: float4 _Color2;
11:
12: CBUFFER_END
以上の対応でSRP Batcherに対応したシェーダーを作成できますが、Inspectorから該当のシェーダーがSRP Batcherに対応しているかどうか確認することもできます。
シェーダーのInspectorのSRP Batcherの項目がcompatibleとなっていたらSRP Batcherに対応しており、not compatibleとなっていたら対応していないことがわかります。
図7.9: SRP Batcherに対応しているシェーダー
2DゲームやUIでは多くのスプライトを使用して画面を構築することがしばしばあります。そういった場合に大量のドローコールを発生させないための機能がSpriteAtlasです。
SpriteAtlasを使用すると、複数のスプライトを1つのテクスチャとしてまとめることでドローコールが削減できます。
SpriteAtlasを作成するには、まずPackage Managerから2D Spriteをプロジェクトにインストールする必要があります。
図7.10: 2D Sprite
インストール後、Projectビューで右クリックして「Create -> 2D -> Sprite Atlas」を選択し、SpriteAtlasのアセットを作成します。
図7.11: SpriteAtlasの作成
アトラス化するスプライトを指定するには、SpriteAtlasのInspectorからObjects for Packingの項目にスプライトかスプライトを含むフォルダーを設定します。
図7.12: Objects for Packingの設定
以上の設定を行うことでビルド時やUnity Editorの再生時にアトラス化の処理が行われ、対象のスプライトの描画の際はSpriteAtlasで統合されたテクスチャが参照されるようになります。
以下のようなコードでSpriteAtlasから直接スプライトを取得することも可能です。
リスト7.5: SpriteAtlasからSpriteをロードする
1: [SerializeField]
2: private SpriteAtlas atlas;
3:
4: public Sprite LoadSprite(string spriteName)
5: {
6: // Sprite名を引数にSpriteAtlasからSpriteを取得する
7: var sprite = atlas.GetSprite(spriteName);
8: return sprite;
9: }
SpriteAtlasに含まれるSpriteを1つロードするとアトラス全体のテクスチャが読み込まれるため、1つだけロードするよりも多くのメモリを消費します。そのため、SpriteAtlasを適切に分割するなど注意して使用する必要があります。
この節はSpriteAtlas V1をターゲットにして記載しています。SpriteAtlas V2ではアトラス化する対象のスプライトのフォルダー指定ができなかったりなど動作に大きく変更が加わる可能性があります。
Unityでは、最終的に画面に表示されない部分の処理を事前に省くためのカリングという処理が行われます。
視錐台カリングとは、カメラの描画範囲となる視錐台の外側にあるオブジェクトを描画対象から省くための処理です。これによりカメラの範囲外のオブジェクトは描画の計算が行われないようになります。
視錐台カリングは何も設定せずともデフォルトで行われています。頂点シェーダーの負荷が高いオブジェクトなどの場合は、メッシュを適切に分割することでカリングの対象とし描画コストを下げる手法も有効です。
背面カリングとは、カメラから見えない(はずの)ポリゴンの裏側を描画から省く処理です。ほとんどのメッシュは閉じている(表側のポリゴンのみがカメラに映る)ため裏側は描画する必要がありません。
Unityではシェーダーに明記しない場合ポリゴンの背面がカリングの対象となっていますが、明記することでカリングの設定を切り替えることが可能です。SubShader内に以下のように記述します。
リスト7.6: カリングの設定
1: SubShader
2: {
3: Tags { "RenderType"="Opaque" }
4: LOD 100
5:
6: Cull Back // Front, Off
7:
8: Pass
9: {
10: CGPROGRAM
11: #pragma vertex vert
12: #pragma fragment frag
13: ENDCG
14: }
15: }
Back、Front、Offの3つの設定がありますが、それぞれの効果は以下のようになっています。
オクルージョンカリングとは、オブジェクトに遮蔽されてカメラに映らないオブジェクトを描画対象から省く処理です。この機能は事前にベイクした遮蔽判定用のデータをもとにランタイムでオブジェクトが遮蔽されているか判定し、遮蔽されているオブジェクトを描画対象から外します。
オブジェクトをオクルージョンカリングの対象とするには、InspectorのstaticフラグからOccluder StaticまたはOccludee Staticを有効にします。Occluder Staticを無効にしOccludee Staticを有効にした場合はオブジェクトを遮蔽する側としては判定されなくなり、遮蔽される側のみとして処理が行われるようになります。逆の場合は遮蔽される側として判定されなくなり、 遮蔽する側のみとして処理されます。
図7.13: オクルージョンカリングのstaticフラグ
オクルージョンカリング用の事前ベイクを行うため、Occlusion Cullingウィンドウを表示します。このウィンドウでは各オブジェクトのstaticフラグの変更やベイクの設定変更などを行うことができ、Bakeボタンを押すことでベイクを行うことができます。
図7.14: Occlusion Cullingウィンドウ
オクルージョンカリングは描画コストを削減する代わりにカリング処理のためにCPUへ負荷をかけるため、各負荷のバランスを見て適切に設定を行う必要があります。
オクルージョンカリングで削減されるのはオブジェクトの描画処理のみで、リアルタイムシャドウの描画などの処理はそのまま行われます。
シェーダーはグラフィックス表現に非常に有効ですが、しばしばパフォーマンスの問題を引き起こします。
GPU(とくにモバイルプラットフォーム)の計算速度は大きいデータ型より小さいデータ型のほうが速くなります。そのため、置き換え可能な場合は浮動小数点数型をfloat型(32bit)からhalf型(16bit)に置き換えることが有効です。
深度計算など精度が必要な場合はfloat型を使うべきですが、Colorの計算などでは精度を落としてしまっても結果的な見た目に大きな差異は起こりづらいです。
頂点シェーダーの処理はメッシュの頂点数分だけ実行され、フラグメントシェーダーの処理は最終的に書き込まれるピクセル数分実行されます。一般的に頂点シェーダーの実行回数はフラグメントシェーダーよりも少ないことが多く、そのため複雑な計算は可能な限り頂点シェーダーで行うのが良いでしょう。
頂点シェーダーの計算結果はシェーダーセマンティクスを介してフラグメントシェーダーに渡されますが、ここで渡される値は補間されたものであり、フラグメントシェーダーで計算した場合と見た目が違う可能性に注意する必要があります。
リスト7.7: 頂点シェーダーによる事前計算
1: CGPROGRAM
2: #pragma vertex vert
3: #pragma fragment frag
4:
5: #include "UnityCG.cginc"
6:
7: struct appdata
8: {
9: float4 vertex : POSITION;
10: float2 uv : TEXCOORD0;
11: };
12:
13: struct v2f
14: {
15: float2 uv : TEXCOORD0;
16: float3 factor : TEXCOORD1;
17: float4 vertex : SV_POSITION;
18: };
19:
20: sampler2D _MainTex;
21: float4 _MainTex_ST;
22:
23: v2f vert (appdata v)
24: {
25: v2f o;
26: o.vertex = UnityObjectToClipPos(v.vertex);
27: o.uv = TRANSFORM_TEX(v.uv, _MainTex);
28:
29: // 複雑な事前計算を行う
30: o.factor = CalculateFactor();
31:
32: return o;
33: }
34:
35: fixed4 frag (v2f i) : SV_Target
36: {
37: fixed4 col = tex2D(_MainTex, i.uv);
38:
39: // 頂点シェーダーで計算した値をフラグメントシェーダーで使用する
40: col *= i.factor;
41:
42: return col;
43: }
44: ENDCG
シェーダー内の複雑な計算の結果がもし外部の値によって変化しないのであれば、テクスチャの要素として事前計算した結果を格納しておくことも有効な手段です。
方法としては、Unityで専用のテクスチャを生成するツールを実装したり各種DCCツールの拡張機能として実装するなどが考えられます。すでに使用しているテクスチャのアルファチャンネルがもし使用されていなければそこに書き込んだり、専用のテクスチャを用意するのが良いでしょう。
たとえば、カラーグレーディングに使用されるLUT(色対応表)は、各ピクセルの座標が各カラーに対応するテクスチャに対して事前に色調補正をかけます。そのテクスチャをシェーダー内で元のカラーをもとにサンプリングすることで、事前にかけた色調補正をもとのカラーに対してかけたのとほぼ同一の結果を得ることができます。
図7.15: 色調補正前のLUTテクスチャ(1024x32)
ShaderVariantCollectionを使用すると、シェーダーを使用する前にコンパイルしスパイクを防ぐことができます。
ShaderVariantColletionではゲーム内で使用するシェーダーバリアントのリストをアセットとして保持できます。Projectビューから「Create -> Shader -> Shader Variant Collection」を選択して作成します。
図7.16: ShaderVariantCollectionの作成
作成したShaderVariantCollectionのInspectorビューからAdd Shaderを押下して対象のシェーダーを追加し、さらにシェーダーに対してどのバリアントを追加するかを選択します。
図7.17: ShaderVariantCollectionのInspector
ShaderVariantCollectionをGraphics SettingsのShader preloadingの項目内のPreloaded Shadersに追加することで、アプリケーションの起動時にコンパイルするシェーダーバリアントを設定できます。
図7.18: Preloaded Shaders
また、スクリプトからShaderVariantCollection.WarmUp()を呼び出すことで該当のShaderVariantCollectionに含まれるシェーダーバリアントを明示的に事前コンパイルすることも可能です。
リスト7.8: ShaderVariantCollection.WarmUp
1: public void PreloadShaderVariants(ShaderVariantCollection collection)
2: {
3: // 明示的にシェーダーバリアントを事前コンパイルする
4: if (!collection.isWarmedUp)
5: {
6: collection.WarmUp();
7: }
8: }
ライティングはゲームのアート表現において非常に重要な要素の1つですが、パフォーマンスに大きく影響を与えることが多いです。
リアルタイムシャドウの生成はドローコールやフィルレートを大きく消費します。そのため、リアルタイムシャドウを使用する際は慎重に設定を検討する必要があります。
シャドウ生成のドローコールを削減するためには、以下のような方針が考えられます。
シャドウを落とすオブジェクトを減らす方法はいくつかありますが、シンプルな方法はMeshRendererのCast Shadowsの設定をオフにすることです。これによりオブジェクトをシャドウの描画対象から外すことができます。この設定は通常Unityではオンになっているため、シャドウを使用しているプロジェクトでは注意するべきでしょう。
図7.19: Cast Shadows
また、オブジェクトがシャドウマップに描画される最大距離を短くすることも有効です。Quality SettingsのShadow Distanceの項目でシャドウマップの最大描画距離を変更することができ、この設定によりシャドウを落とすオブジェクトの数を必要最低限に減らすことができます。この設定を調整することでシャドウマップの解像度に対して最低限の範囲でシャドウを描画することができるため、シャドウの解像感の低下を抑えることにも繋がります。
図7.20: Shadow Distance
通常の描画と同様、シャドウ描画でもバッチング処理の対象にすることでドローコールを削減できます。バッチングの手法については「7.3 ドローコールの削減」を参照してください。
シャドウによるフィルレートはシャドウマップの描画とシャドウの影響を受けるオブジェクトの描画の両方に左右されます。
Quality SettingsのShadowsの項目にあるいくつかの設定を調整することでそれぞれのフィルレートを節約できます。
図7.21: Quality Settings -> Shadows
Shadowsの項目ではシャドウの形式を変更することができ、Hard Shadowsは影の境界線がはっきりと出ますが比較的負荷が低く、Soft Shadowsは影の境界線をぼかしたような表現ができますが負荷が高いです。
Shadow ResolutionとShadow Cascadesの項目はシャドウマップの解像度に影響する項目で、大きく設定するとシャドウマップの解像度が大きくなりフィルレートの消費が大きくなります。ただ、この設定はシャドウの品質に大きく関係するところでもあるため、パフォーマンスと品質のバランスを見ながら慎重に調整する必要があります。
一部の設定はLightコンポーネントのInspectorから変更できるため、個別のライトごとに設定を変えることも可能です。
図7.22: Lightコンポーネントのシャドウ設定
ゲームジャンルやアートスタイルによっては、板ポリゴンなどでオブジェクトの影を擬似的に表現する手法も有効です。この手法は使用上の制約が強く自由度が高いものではありませんが、通常のリアルタイムシャドウの描画手法に比べて圧倒的に軽量に描画できます。
図7.23: 板ポリゴンによる擬似シャドウ
事前にライティングの効果とシャドウをテクスチャにベイクしておくことで、リアルタイム生成よりもかなり低負荷に品質の高いライティング表現を実現できます。
ライトマップをベイクするには、まずシーンに配置したLightコンポーネントのModeの項目をMixedかBakedに変更します。
図7.24: LightのMode設定
また、ベイク対象となるオブジェクトのstaticフラグを有効化します。
図7.25: staticの有効化
この状態で、メニューから「Window -> Rendering -> Lighting」を選択しLightingビューを表示します。
デフォルトの状態だとLighting Settingsアセットが指定されていないため、New Lighting Settingsボタンを押下して新規作成を行います。
図7.26: New Lighting Settings
ライトマップについての設定は主にLightmapping Settingsタブで行います。
図7.27: Lightmapping Settings
多くの設定項目がありますが、これらの値を調整することでライトマップのベイクの速度や品質が変わります。そのため、求める速度や品質に合わせて適切に設定する必要があります。
これらの設定の中でもっともパフォーマンスへの影響が大きいのはLightmap Resolutionです。この設定はUnityにおける1unitあたりにライトマップのテクセルをどれだけ割り当てるかという設定で、この値により最終的なライトマップのサイズが変動するため、ストレージやメモリの容量、テクスチャへのアクセス速度などに大きな影響を与えます。
図7.28: Lightmap Resolution
最後に、Inspectorビューの下部にあるGenerate Lightingボタンを押下することでライトマップのベイクを行うことができます。ベイクが完了すると、シーンと同名のフォルダーにベイクされたライトマップが格納されていることを確認できます。
図7.29: Generate Lighting
図7.30: ベイクされたライトマップ
カメラから遠い距離にあるオブジェクトをハイポリゴン・高精細に描画するのは非効率です。Level of Detail(LOD)という手法を利用することでカメラからの距離に応じてオブジェクトの詳細度を削減できます。
Unityでは、オブジェクトにLOD Groupコンポーネントを追加することでLODの制御を行うことができます。
図7.31: LOD Group
LOD GroupがアタッチされたGameObjectの子に各LODレベルのメッシュを持ったRendererを配置し、LOD Groupの各LODレベルに設定することでカメラに応じてLODレベルが切り替えられるようになります。カメラの距離に対してどのLODレベルを割り当てるかをLOD Groupごとに設定することも可能です。
LODを使用することで一般に描画の負荷を削減できますが、各LODレベルのメッシュがすべてロードされるためメモリやストレージの圧迫には注意が必要です。
Unityのテクスチャストリーミングを利用することで、テクスチャのために必要なメモリ容量やロード時間を削減できます。テクスチャストリーミングとは、シーンのカメラ位置に応じてミップマップをロードすることでGPUメモリを節約するための機能です。
この機能を有効にするには、Quality SettingsのTexture Streamingを有効化します。
図7.32: Texture Streaming
さらに、テクスチャのミップマップをストリーミングできるようにするためテクスチャのインポート設定を変更する必要があります。テクスチャのInspectorを開き、Advanced設定内のStreaming Mipmapsを有効化します。
図7.33: Streaming Mipmaps
これらの設定により指定されたテクスチャのミップマップがストリーミングされるようになります。またQuality SettingsのMemory Budgetの項目を調整することでロードするテクスチャの合計メモリ使用量を制限できます。テクスチャストリーミングシステムはここで設定したメモリ量を超えないようにミップマップをロードします。