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
}

f:id:enrike3:20180310152613p:plain

このコードで大事なところは描画のタイミングで、これは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にのみ適用されます

このあたりのパラメータがしっかり一致していればちゃんとバッチされます。 f:id:enrike3:20180310153819p:plain

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