UIの頂点をカスタムする~ModifyVertices、ModifyMeshのバージョンアップ対応~


2015年のUniteでuGUIのUIの頂点をカスタムする拡張スクリプトの書き方の例として、「太いアウトラインを表示する」スクリプトの書き方を発表しました。詳しくはこちら
宴にも組み込まれている「RichOutline」コンポーネントです。


UIの頂点をカスタムする際の要点としては、「ModifyVerticesをoverrideすればUIの頂点が自由にカスタムできる」ということだったのですが、その後のUnityのバージョンアップでModifyVerticesが使えなくなってしまいました。
しかも、Unity5.1以前、Unity5.2.0、Unity5.2.1以降で微妙に書き方が違うため、けっこう面倒なことになっています。

・・・というわけで、Unityのバージョンアップに対応したスクリプトを公開します。


using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;


namespace Utage
{

	/// <summary>
	/// アウトラインコンポーネントをリッチに
	/// </summary>
	[AddComponentMenu("Utage/Lib/UI/RichOutline")]
	public class UguiRichOutline : Outline
	{
		public int copyCount = 16;
#if UNITY_4_6 || UNITY_5_0 || UNITY_5_1
        public override void ModifyVertices(List<UIVertex> verts)
		{
			if (!IsActive())
				return;

            ModifyVerticesSub(verts);
        }
#elif UNITY_5_2_0
        public override void ModifyMesh(Mesh mesh)
        {
            if (!IsActive())
                return;

            var verts = new List<UIVertex>();
            using (var helper = new VertexHelper(mesh))
            {
                helper.GetUIVertexStream(verts);
            }

            ModifyVerticesSub(verts);

            using (var helper2 = new VertexHelper())
            {
                helper2.AddUIVertexTriangleStream(verts);
                helper2.FillMesh(mesh);
            }
        }
#else
        public override void ModifyMesh(VertexHelper vh)
        {
            if (!IsActive())
                return;

            var verts = new List<UIVertex>();
            vh.GetUIVertexStream(verts);

            ModifyVerticesSub(verts);

            vh.Clear();
            vh.AddUIVertexTriangleStream(verts);
        }
#endif

        void ModifyVerticesSub(List<UIVertex> verts)
        {
            var start = 0;
            var end = verts.Count;

            for (int i = 0; i < copyCount; ++i)
            {
                float x = Mathf.Sin(Mathf.PI * 2 * i / copyCount) * effectDistance.x;
                float y = Mathf.Cos(Mathf.PI * 2 * i / copyCount) * effectDistance.y;
                ApplyShadow(verts, effectColor, start, verts.Count, x, y);
                start = end;
                end = verts.Count;
            }
        }
    }
}

ネームスペースがUtageになっていますが、自分のプロジェクトで使うならその辺は好きに変えてしまってください。ライセンスフリーでご自由にどうぞ。
また、Unity5.2.1はパッチ版かどうかで上記の仕様が違うので、完全対応ができませんのでご注意を。

追記 処理が重い?

上記のコードのうち、Unity5.2以降の記述のほうでは、実はけっこうな負荷がかかることがあります。
原因は var verts = new List(); の部分です。

頂点計算のような「一時的だけどたくさんnew(インスタンスの作成・メモリ確保)する可能性がある」ものの場合、毎フレームnewを走らせてしまうと、GCがすぐに発生して処理が遅くなってしまいます。

しかも、この場合は一時的に計算するためのバッファとしてnewしていて、メソッド内のローカル変数としてすぐに開放されてしまいます。
こういった「大量に確保する必要のある一時領域」を毎フレーム作っては開放していると、GCが頻発することになってしまいます。

ではどうすればいいかというと、Bitbucketで公開れているUnityのコードではこのようになっています。

        public override void ModifyMesh(VertexHelper vh)
        {
            if (!IsActive())
                return;

            var output = ListPool<UIVertex>.Get();
            vh.GetUIVertexStream(output);

            ApplyShadow(output, effectColor, 0, output.Count, effectDistance.x, effectDistance.y);
            vh.Clear();
            vh.AddUIVertexTriangleStream(output);
            ListPool<UIVertex>.Release(output);
        }

ListではなくListPoolというのを使っています。
これは、「一度作ったオブジェクトをstatic領域に確保したまま使いまわす」というプール処理をするためのクラスだと思われます。
これを使うことで、毎フレームnewされることなく、GCが頻繁に走ることも防げます。
ただし、このListPoolはUnityのInternalな非公開クラスになっているので、外部からは使えません。
ソースはこちらに公開されているので、それを使って自分でクラスを書いても良いでしょうし、似たようなプール処理をするクラスを自作しても良いでしょう。

UIの頂点アニメーションをするなど、毎フレーム計算させるような場合は上記のListPoolのような処理に切り替えることをお勧めします。