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