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