Materialエディタの拡張
の記事を読んで「よし、俺も統一シェーダ作るぞ!」と思って実際にやってみると、
シェーダのコードを#ifdefで切り刻むこと自体はそこまで大変ではないですが、
「エディタのほうはしんどそう」…と思っていました。
たとえば、画像を別画像の形状で切り抜くオプションを付けたとします。
あくまでオプションなのでこの機能を使わないときはインスペクタにマスク画像を表示したくない。 機能をONにしたときだけ表示してほしい。
オプションの機能が一つだけなら表示しちゃって気にしないという手はあるのですが、オプションが増えてきて、UVScrollだとかブレンド方式切り替えだとか、カラーチャンネルも切り替えだとかやりはじめるとやはり限界でして、表示のON/OFFを切り替えたい。ぜひに切り替えたい。
これをやるには、MaterialのInspectorを乗っ取る必要があるだろうから気が重かったのですが、Unity5以降のUnityEditor.ShaderGUI
クラスを拡張すると意外にも簡単にできました。UnityEditor.MaterialEditor
を継承する方式だと難しそうだったのに。ShaderGUI最高か。
方法
まずシェーダのCustomEditorに、ShaderGUIを継承するオレオレエディタのクラス名を名前空間つきで書きます。
Shader "Hoge/OreOreSprite" { Properties { … } SubShader { … } CustomEditor "HogeHoge.OreOreShaderGUI" }
MaterialEditorは名前空間使えなかったので、この時点で好感度急上昇。
そして、実装を書きます。
using UnityEditor; namespace HogeHoge { public class OreOreShaderGUI : ShaderGUI { public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) { } } }
これだけで、インスペクタの乗っ取りは済んでいます。OnGUIが空なのでインスペクタはまっさらになります。
乗っ取れたので次は中身を書いていくわけですが、
引数で渡ってくるMaterialEditorとMaterialPropertyを使えばプロパティの表示はとても楽です。
たとえば、MainTexを表示するには、propertiesからMainTexを探してmaterialEditor.ShaderProperty()
に食べさせるだけで済みます。
using System.Linq; using UnityEditor; namespace HogeHoge { public class OreOreShaderGUI : ShaderGUI { public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) { var mainTexProp = properties.First(x => x.name == "_MainTex"); materialEditor.ShaderProperty(mainTexProp, mainTexProp.displayName); } } }
ここまでくれば、あとは普通のエディタ拡張と変わりません。
Toggleを使って表示非表示を制御するだけです。
色気をだして、Boxでかこったり、Toggleのラベルを太字にしたり、ということもできますね。
using System.Linq; using UnityEngine; using UnityEditor; namespace HogeHoge { public class OreOreShaderGUI : ShaderGUI { const string AlphaTexEnabledKeyword = "_ENABLE_ALPHA_TEX"; public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) { var material = (Material)materialEditor.target; var mainTexProp = properties.First(x => x.name == "_MainTex"); materialEditor.ShaderProperty(mainTexProp, mainTexProp.displayName); var colorProp = properties.First(x => x.name == "_Color"); materialEditor.ShaderProperty(colorProp, colorProp.displayName); // // Alpha Texture // using (new EditorGUILayout.VerticalScope("box")) { EditorGUI.BeginChangeCheck(); var origFontStyle = EditorStyles.label.fontStyle; EditorStyles.label.fontStyle = FontStyle.Bold; var alphaTexEnabled = EditorGUILayout.Toggle("Enable Alpha Texture", material.IsKeywordEnabled(AlphaTexEnabledKeyword)); EditorStyles.label.fontStyle = origFontStyle; if (EditorGUI.EndChangeCheck()) { if (alphaTexEnabled) material.EnableKeyword(AlphaTexEnabledKeyword); else material.DisableKeyword(AlphaTexEnabledKeyword); } if (alphaTexEnabled) { var alphaTexProp = properties.First(x => x.name == "_AlphaTex"); materialEditor.ShaderProperty(alphaTexProp, alphaTexProp.displayName); } } } } }
なんかテクスチャ設定するところが細長くなってるのが気になるけど、まあ普段使いには支障ないレベルでしょう。
Snapdragon Profiler
UnityのProfiler上ではGPUの細かいメトリクスはとれないので、描画負荷をはかるためには Androidなら各SoC向けのプロファイラ。iOSならInstrumentsを使って描画負荷を測ることになります。
自分は私物端末がSnapdragonなので、普段はSnapdragonProfilerを見ています。
Snapdragon Profiler - Qualcomm
ダウンロードするためにはQualcommにユーザー登録が必要です。
普段よくみる指標
仕事で2Dのゲームを作っていたので、見るところは
- GPU Memory Stats
- Texture Memory Read
- Read Total
- Write Total
と、テクスチャの読み込みやフィルの周りにしています。
次点で
- GPU Stalls
- Texture Fetch Stall
- Texture L1 Miss
- Texture L2 Miss
あたりです。
↑の図は2048x2048の画像をTruecolorからETC2_4bppに切り替えた瞬間のナイアガラ
24bit per pixel → 4bppなので1/6ですが、実際のTexture ReadやReadTotalの測定結果は1/10近く行ったりします。
このあたりは測らないとわからない。
一方でWrite Totalは解像度とoverdraw依存、つまり塗りつぶすピクセルの量に依存するので 圧縮テクスチャを使っても下がらないことがわかります。
overdrawはUnityEditor上で視覚的に見ることができますが、
エフェクトを作成する人に「overdraw控えめにしてね!」とどんなに口酸っぱく言っても各人の感覚によってしまうので
「これぐらいはギリギリ許されるだろう」と許されざるエフェクトが上がってくることがあります。
こういうときはやっぱり数値で落とせると強い(はず) 。
「このシーンに置いたときにWrite Totalが500MB超えたらNGとします」
みたいなやりとりができるようになります。
この手の数値は普段からいろんなものを眺めていないと「1G超えはヤバい!」みたいな感覚が養われないので
性能面で追い詰められてからみても、なるほどよくわからん、にしかならないので
普段からいろいろ眺めておきたいなぁと思っております。
RenderDocがとても良い
RenderDoc神。
UnityEditor上でグラフィックがバグってて困ったときはまずFrameDebuggerでぱぱっと確認するわけですが、FrameDebuggerで原因をつき止められない場合はRenderDocが助けになります。
Visual Studioのグラフィック診断相当の機能が少ない手順でさくさくと使えます。
キャプチャはとても簡単。
RenderDocをインストールしたら、UnityのGameViewでPlay前にRenderDocをLoadしておきます。
あとは好きなタイミングでキャプチャボタンぽちるだけ
画面
PixelHistoryで、画面のポイントがどう塗りつぶされたかの履歴を見ることができたり
Meshの中身の値を見ることができたりします。
シェーダーのデバッグはかどる
サイコーです
ETC2はETC1と比べてカラー部分も改善されているという話
社内で圧縮テクスチャの話をしたのでブログでも少々。
DXT1からつらなる初期の圧縮テクスチャの系譜は4x4=16pxを1ブロックとして扱い
1ブロックあたりカラー(RGB)を64bit、すなわち64/16=4bpp (bit per pixel)としています。
これだとアルファがもてないので、1ブロックあたりアルファも64bit (4bpp)足して
- 不透明画像 4bpp
- 透明度ありの画像 8bpp
というのがDXT2~5とETC2の構成です。
なおPVRTCは4bppの中でアルファも表現しているので透明度をもつブロックはカラー品質が厳しいことになります。
RGB888(24bpp)に対する4bpp、すなわち圧縮比率1/6という厳しい制約の中で
どのようなアルゴリズムで圧縮をしているかは、OPTPiXのブログに素晴らしい解説があります。
ETCの改善
ETC1の利点は、輝度のテーブルで白と黒を作れるので、
DXT1の弱点であった1ブロックあたり傾向の異なる(代表色のブレンドで表現できない)色が3色以上あると詰む問題に対して、
白や黒ならば色が増えても上手くさばけること。サブブロック分割があるのでブロック感がDXT1よりやわらぐこと。
一方でETC1の弱点はOPTPiXのブログにあるとおり異なる色相が斜めっている場合で、
これはサブブロックを縦に割っても横に割ってもうまくいかないため圧縮テクスチャの弱点である色のはみ出しがとても顕著にでます。
ETC1を見ればUnityちゃんの左腕や、bしてる右腕の袖のあたりに色のはみ出しが見えると思います。
素朴なDXT1では特に弱点にはならないのにETC1ではダメ、というのがなんとも切ない。
ETC2はETC1のもつ弱点にしっかり取り組んでいます。
サブブロック分割を伴わない三つのモードが新たに追加され、ETC1で弱かった絵が顕著に改善されています。
詳細は以下の資料に詳しい。
透明度を持てるようになっただけじゃないのよ。
ETC2: Texture Compression using Invalid Combinations
BC7やASTCのような、1ブロックあたり128bitの新世代の圧縮と比べればやや品質は落ちるようですが、
それでも2018年時点で広く使えてなかなかの品質、ということでETC2は良いフォーマットなんじゃないかなと思います。
AndroidにASTCが広まるのはまだ少し時間がかかると思うので、それまでの現実解として付き合っていくことになるでしょう。
©UTJ/UCL
CanvasUpdateRegistryとICanvasElement
UnityEngine.UI.dllの中身はBitbucketにあります。誰でも読めて素晴らしみあります。
uGUIを理解するのにあたって個人的にとても大事だと思っているのは、UnityEngine.UI.CanvasUpdateRegistryクラスです。
こいつは何者なのかというと、ざっくり言ってしまうと前回の記事に書いたCanvas.willRenderCanvases
イベントを、レイアウトとレンダリングの二つに分割するのが役割です。
uGUIにはVerticalLayoutGroupのようなレイアウトコンポーネントが存在します。
もし、描画コンポーネントがCanvasRendererにMeshを送り込んだあとでVerticalLayoutGroupがレイアウトを調整してしまった場合、1フレーム描画がおかしくなります。
したがってレイアウトは必ず描画より先に行われる必要があります。Canvas.willRenderCanvasesイベントより先に発火することが保証された別のレイアウト用イベントを用意するなど実現方法はいろいろあるでしょうが、uGUIはCanvas.willRenderCanvasesを二つにカチ割ることを選びました。
- 再レイアウトが必要なuGUIのコンポーネントは、CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuildで自身をCanvasUpdateRegistryに登録します
- 再描画が必要なuGUIのコンポーネントは、CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuildで自身をCanvasUpdateRegistryに登録します
- CanvasUpdateRegistryはCanvas.willRenderCanvasesのタイミングでレイアウト用に登録されたコンポーネント達にレイアウト指示を送ります。レイアウトが終わったら、再描画用に登録されたコンポーネント達に再描画指示を送ります
- レイアウトや再描画の登録はCanvasUpdateRegistry側で勝手に解除されるので、登録したフレームで一度実行されておしまいです
ICanvasElement
CanvasUpdateRegistryに登録できるのは、UnityEngine.UI.ICanvasElementインターフェースを実装したクラスならなんでもOKです。MonoBehaviourですらない、プレーンなC#クラスでOK。
(※ただしプレーンなクラスを用いる場合はtransformやIsDestroyedの実装で困らないようにMonoBehaviourをラップする形になるのがほとんどだと思われますが)
uGUIのコンポーネントは基底クラスのUnityEngine.UI.GraphicクラスがICanvasElementを実装していますし、レイアウトコンポーネントもUnityEngine.UI.LayoutRebuilderクラスに勝手にラップされるので、uGUIを普段使用するうえでICanvasElementインターフェースの実装を意識することはまずありません。
ですが、「シーンのヒエラルキー上は親子関係にはないがuGUIのコンポーネントと位置を同期しなくてはいけない奴がいてしかもuGUI側が自動レイアウトされる」みたいな面倒ケースでは、ICanvasElementの実装もやむなし!となることもあります。僕は最近遭遇しましたよ、ええ…
ICanvasElementの実装で一番ポイントとなるのはICanvasElement.Rebuildメソッドです。これがレイアウトや再描画の本体になります。現在のRebuildがレイアウト用に呼ばれているかは、引数で渡ってくるUnityEngine.UI.CanvasUpdate列挙型を見ればわかります。わかるのですが、この列挙型はレイアウト用に三つ、描画用に二つのメンバーがいます。
わかりやすくはないのですが、RegisterCanvasElementForLayoutRebuildでコンポーネントを登録すると、1フレ中にPreLayout, Layout, PostLayoutで3回Rebuildが呼ばれ、RegisterCanvasElementForGraphicRebuildでコンポーネントを登録すると、PreRenderとLatePreRenderの2回Rebuildが呼ばれます。(PostRenderじゃないのかよという気もしますがまあMeshをCanvasRendererに突っ込む行為は厳密にはRenderingではないので。この記事内で「再描画」と書いているのも本当は正しくないですね)
実装
UnityEngine.UIを使わずにCanvasに描画するで書いたRawImageModokiを、ICanvasElement対応してみます。
using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(CanvasRenderer))] public class RawImageModoki : MonoBehaviour, ICanvasElement { [SerializeField] private Texture2D _texture = null; public Texture2D Texture { get { return _texture; } set { if(_texture != value) { _texture = value; SetDirty(); } } } private CanvasRenderer _canvasRenderer = null; public CanvasRenderer CanvasRenderer { get { return _canvasRenderer != null ? _canvasRenderer : (_canvasRenderer = GetComponent<CanvasRenderer>()); } } void OnEnable() { SetDirty(); } private void Start() { StartCoroutine(DelayRegister()); } //なぜかIsDestroyedがtrueを返すタイミングがあるので少し遅らせて登録ナンデェ… private System.Collections.IEnumerator DelayRegister() { yield return null; SetDirty(); } private void SetDirty() { CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this); Debug.Log("SetDirty"); } private void Canvas_willRenderCanvases() { const float size = 200; var mesh = new Mesh(); mesh.vertices = new Vector3[] { new Vector3(-size, -size), new Vector3(-size, size), new Vector3( size, size), new Vector3( size, -size), }; mesh.uv = new Vector2[] { new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0), }; mesh.triangles = new int[] { 0, 1, 2, 2, 3, 0 }; var renderer = this.CanvasRenderer; renderer.SetMesh(mesh); renderer.materialCount = 1; //これ忘れると描画されないので注意 renderer.SetMaterial(Canvas.GetDefaultCanvasMaterial(), 0); renderer.SetTexture(this.Texture); renderer.SetColor(Color.white); } #region ICanvasElement void ICanvasElement.Rebuild(CanvasUpdate executing) { Debug.LogFormat("Rebuild {0}", executing); if (executing == CanvasUpdate.PreRender) Canvas_willRenderCanvases(); } public void LayoutComplete() { } public void GraphicUpdateComplete() { } public bool IsDestroyed() { var result = this == null; Debug.LogFormat("IsDestroyed {0}", result); return result; } #endregion #if UNITY_EDITOR private void OnValidate() { SetDirty(); if (!Application.isPlaying) { Canvas_willRenderCanvases(); } } #endif }
Canvas.willRenderCanvasesを直接購読しなくてもよくなったので、_renderRequiredフラグで毎フレームの処理をガードする必要はなくなりました。 Start付近に悲しい小細工がありますが…エディタのみの挙動ですかね…ちょっとよくわからない。
まとめ
- uGUIのコンポーネントはCanvas.willRenderCanvasesイベントを直接購読するのではなく、CanvasUpdateRegistryを経由することでレイアウトとレンダリングのタイミングを分離している。これによって柔軟なレンダリングが可能になる
- ICanvaElementを実装すればCanvasUpdateRegistryに登録できて、uGUIのコンポーネントとイベントの足並みをそろえたり割り込んだりできる
@UTJ/UCL
UnityEngine.UIを使わずにCanvasに描画する
サンプルの実用度はまるで無いのですが、uGUIの理解のために。
UnityEngine.UI.dllはC#で書かれたManagedなdllです。
一方、Canvas, CanvasGroup, CanvasRendererなどはネイティブコンポーネントであり、名前空間もUnityEngineに属します。
CanvasはUnityEngine.UI.dllに依存を持たず、Canvasに描画したければCanvasRendererを使うだけでOKです。
using UnityEngine; [RequireComponent(typeof(CanvasRenderer))] public class RawImageModoki : MonoBehaviour { [SerializeField] private Texture2D _texture = null; public Texture2D Texture { get { return _texture; } set { if(_texture != value) { _renderRequired = true; _texture = value; } } } private CanvasRenderer _canvasRenderer = null; public CanvasRenderer CanvasRenderer { get { return _canvasRenderer != null ? _canvasRenderer : (_canvasRenderer = GetComponent<CanvasRenderer>()); } } void Start () { Canvas.willRenderCanvases += Canvas_willRenderCanvases; } private void OnDestroy() { Canvas.willRenderCanvases -= Canvas_willRenderCanvases; } private void Canvas_willRenderCanvases() { if (!_renderRequired) return; _renderRequired = false; const float size = 200; var mesh = new Mesh(); mesh.vertices = new Vector3[] { new Vector3(-size, -size), new Vector3(-size, size), new Vector3( size, size), new Vector3( size, -size), }; mesh.uv = new Vector2[] { new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0), }; mesh.triangles = new int[] { 0, 1, 2, 2, 3, 0 }; var renderer = this.CanvasRenderer; renderer.SetMesh(mesh); renderer.materialCount = 1; //これ忘れると描画されないので注意 renderer.SetMaterial(Canvas.GetDefaultCanvasMaterial(), 0); renderer.SetTexture(this.Texture); renderer.SetColor(Color.white); } private bool _renderRequired = false; #if UNITY_EDITOR private void OnValidate() { _renderRequired = true; if(!Application.isPlaying) { Canvas_willRenderCanvases(); } } #endif }
このコードで大事なところは描画のタイミングで、これはCanvas.willRenderCanvases
イベントを使います。厳密には調べていませんが、このイベントはLateUpdateより後に毎フレーム発火する雰囲気です。
void Start ()
{
Canvas.willRenderCanvases += Canvas_willRenderCanvases;
}
あとは、このイベントの中でCanvasRendererに描画に必要なものを詰めるだけです。
var renderer = this.CanvasRenderer; renderer.SetMesh(mesh); renderer.materialCount = 1; //これ忘れると描画されないので注意 renderer.SetMaterial(Canvas.GetDefaultCanvasMaterial(), 0); renderer.SetTexture(this.Texture); renderer.SetColor(Color.white);
ただしCanvas.willRenderCanvases
は毎フレーム飛んでくるので、こちらに変更があるときだけCanvasRendererを更新するようなコードにします。
Canvasは、UnityのDynamicBatchingとは別枠でCanvasRenderer達の描画をバッチングしますが、バッチ条件は似たようなものなので、同一のMaterial、かつパラメータ一致意識しておけばOKです。CanvasRendererはMaterialPropertyBlockと似たような役割も果たすため、以下のメソッドはMaterialPropertyBlockと同じ感覚で使ってよいです。ただしシェーダパラメータ名は指定できず、適用先が固定です
- CanvasRenderer.SetTexture ※_MainTexに挿さります
- CanvasRenderer.SetAlphaTexture ※_AlphaTexに挿さります
- CanvasRenderer.SetColor ※_Colorに適用されます
- CanvasRenderer.SetAlpha ※_Colorのaにのみ適用されます
このあたりのパラメータがしっかり一致していればちゃんとバッチされます。
Canvasは一度バッチした結果を毎フレーム使いまわして描画しますが、CanvasRendererに更新がかかればもちろん再度バッチする必要があります。
これはProfiler上はCanvas.BuildBatch
で見えます。ですがバッチ処理はUnity5.2でかなり最適化されたようでCanvas.BuildBatch
が大きな数字になることはさほどないように思います。
経験上、Profiler上で目立つのはCanvas.SendWillRenderCanvases()
のほうです。
サンプルからもわかる通りこれはCanvasRendererに食べさせるMeshを生成するコードになりますが、UI要素が多いシーンの最初のフレームで顕著なスパイクを作ることがあります。UIもフレームを分けて生成できるように組んだ方がFPSの維持には良いであろうと思います。
もちろんフレームを分けることでBuildBatchの回数は増えますが、BuildBatchはかなり高速なので、初期化時の数回くらいは気にならないと思います。
さすがに毎フレーム変化するアニメーションモノは別Canvasに隔離したり、そもそもCanvasを使わずSpriteRendererにしたりすべきだとは思いますが。
まとめ
脱線はしましたが、まとめとしては
CanvasRendererだけあればCanvasに描画できますよ
ということだけです。
ではuGUIであるところのUnityEngine.UI.dllは一体何をしてくれるのかというと、CanvasRendererにMeshなどを食べさせるところをとても使いやすくしてくれているユーティリティないしフレームワークということになります。
@UTJ/UCL
DrawCall Batching
DrawCall Batching
Unityは描画効率のために、複数のRendererの描画を一つに束ねることがあります。 これをバッチングといい、 どのRendererの描画をまとめるかを事前に指定する方式をstatic batching Materialの状況をみてUnityがよしなに描画を束ねるかどうかを自動で判断するのをdynamic batchingといいます。 バッチングのON/OFFはPlayer Settingsで指定でき、デフォルトはONです。
dyanmic batchingの条件
dynamic batchingの効く条件は、以下2点を満たすことです
- 同一のマテリアルを使用していること
- Material Property Blockの値が一致していること
あ、もちろん同一のCameraで映してくださいね。
実験
どシンプルなSprite風シェーダで実験です。 このシェーダは画像と乗算カラーのみを指定できます。
Shader "Hoge/OreOreSprite" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color("Tint", Color) = (1,1,1,1) } SubShader { Cull Off ZWrite Off ZTest Always Blend One OneMinusSrcAlpha Tags { "Queue" = "Transparent" "PreviewType" = "Plane" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float4 _Color; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float4 color : COLOR; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.color = v.color * _Color; return o; } sampler2D _MainTex; fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv) * i.color; return col; } ENDCG } } }
作ったShaderにunity-chanの画像でもぶっ挿して乗算カラーも指定しておきます。
このMaterialを使用して適当にQuadメッシュを二つ描画してみます。 FrameDebuggerでみると、ちゃんとバッチが効いて1Drawで2つの描画が束ねられています。
ここで、2体目のUnity-ChanのMaterialを差し替えてみましょう。 元のMaterialをコピーして、同じシェーダ、同じテクスチャ、同じ乗算カラーのMaterialを作成します。 中身は一緒だけどガワが違うとでもいいましょうか。
描画は分かれてしまいました。 中身がどんなに等しくても、Materialが別だとバッチングは効かないことがわかります。
Material Property Block
Unityには、同一のMaterialを使用しつつも、Renderer単位でパラメータを変える仕組みとして
Material Property Block
が存在します。
すんごい雑にStartでMaterialPropertyBlockをつっこむコンポーネントを書いてみます。
using UnityEngine; [RequireComponent(typeof(MeshRenderer))] public class OreOreSprite : MonoBehaviour { [SerializeField] private Texture _texture = null; [SerializeField] private Color _color; private MeshRenderer _renderer = null; public MeshRenderer Renderer { get { return _renderer != null ? _renderer : (_renderer = GetComponent<MeshRenderer>()); } } private MaterialPropertyBlock _block = null; void Start () { _block = new MaterialPropertyBlock(); _block.SetTexture("_MainTex", _texture); _block.SetColor("_Color", _color); this.Renderer.SetPropertyBlock(_block); } }
インスペクタでテクスチャとカラーを指定可能
これで、同一のMaterialを使いながらもパラメータを分けることができるようになりました。 同一のマテリアルを使用していてもパラメータが異なることによりバッチングは効いていません。
この状態で、Inspectorから同じ画像、同じ色を指定してみましょう。(Materialは同一のものを使用するとします)
無事描画が一つにまとまりました。
まとめ
dynamic batchingの効く条件は、以下2点を満たすことです
- 同一のマテリアルを使用していること
- Material Property Blockの値が一致していること
今回はMeshRendererを使用しましたが、SpriteRendererやCanvasあたりも基本的には同じです。 (Canvasはdyanmic batchingというよりはCanvas独自バッチングになるのでむしろdynamic batchingからははずれますが、Materialが分かれたりパラメータがばらけるとバッチされないという点では一緒) MaterialPropertyBlockによって、同一Materialを使いまわしながらも、パラメータを変更できるようにし、 MaterialPropertyBlockのパラメータが(たまたまor狙って)一致した場合は描画が束ねられる、という形をとっています。 なおuGUIの場合はCanvasRendererがMaterialPropertyBlockに近い役割を担っています。
テクスチャなんて一致するのかよ、という点については、 複数の画像を一つにパッキングしておけば束ねられます。
こうやってパックされたテクスチャを使って、Spriteによる範囲指定で抜けば、画像内のキャラを何体書いても同一Materialを使う限り1DrawCallで済みます。 キャラクターは画像が大きいことが多いので例として良くないですが、 小さい画像の多いUIではパッキングは効果があります。
余談:PerRendererDataについて
MaterialPropertyBlockやCanvasRendererによって、後から画像を挿す場合Materialに挿してある画像は無駄です。 消し忘れると、MaterialをLoadしただけで使う予定のない画像までついてくる。許せん、となります。 そのような想定の画像はshader側で [PerRendererData] 指定をしておくと良いです。
Shader "Hoge/OreOreSprite" { Properties { [PerRendererData] _MainTex ("Texture", 2D) = "white" {} _Color("Tint", Color) = (1,1,1,1) }
これを書くと、Materialのインスペクタから _MainTexを指す箇所が消えるので事故防止になります。
© UTJ/UCL