【Unity】【UI Toolkit】VisualElementで描画のためのメッシュを自由に生成できるMeshGenerationContextの使い方

UnityのUI ToolkitでVisualElementで描画のためのメッシュを自由に生成できるMeshGenerationContextの使い方についてまとめました。

Unity 2022.2.19

MeshGenerationContextとは

UI ToolkitでUIを描画する際には、内部的にそのUIを描画するためのメッシュが生成されます。

この処理は通常は自動的に行われますが、VisualElement.generateVisualContentにコールバックを登録することで、任意のメッシュを生成することができます。
この時のコールバック関数の引数として渡されるのがMeshGenerateContextです。
このクラスにメッシュの設定することで、UI領域にそのメッシュが描画されます。

なおVisualElement.generateVisualContentによる再描画は、ビジュアルに影響する VisualElement のプロパティに変更が加わったり、VisualElement.MarkDirtyRepaintが呼ばれると発生します。

使ってみる

では実際にこれを使って独自のメッシュを使ってUIを描画してみます。

using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleElement : VisualElement
{
    public ExampleElement()
    {
        // 描画用メッシュを生成する際のコールバックを登録
        generateVisualContent += OnGenerateVisualContent;
    }

    private void OnGenerateVisualContent(MeshGenerationContext context)
    {
        // 小さすぎて見えないレベルだったら描画しない
        if (contentRect.width < 0.01f || contentRect.height < 0.01f)
            return;

        var vertices = new Vertex[4];
        var indices = new ushort[] { 0, 1, 2, 2, 3, 0 };

        // アロケート
        var mesh = context.Allocate(4, 6);

        // 頂点座標を作成
        {
            var left = 0.0f;
            var right = contentRect.width;
            var top = 0.0f;
            var bottom = contentRect.height;
            vertices[0].position = new Vector3(left, bottom, Vertex.nearZ);
            vertices[1].position = new Vector3(left, top, Vertex.nearZ);
            vertices[2].position = new Vector3(right, top, Vertex.nearZ);
            vertices[3].position = new Vector3(right, bottom, Vertex.nearZ);
        }

        // 頂点カラーを作成
        {
            vertices[0].tint = Color.red;
            vertices[1].tint = Color.green;
            vertices[2].tint = Color.blue;
            vertices[3].tint = Color.black;
        }

        // 頂点インデックスを作成
        {
            indices[0] = 0;
            indices[1] = 1;
            indices[2] = 2;
            indices[3] = 2;
            indices[4] = 3;
            indices[5] = 0;
        }

        // 頂点と頂点インデックスを設定
        mesh.SetAllVertices(vertices);
        mesh.SetAllIndices(indices);
    }
}

説明はコメントに書いたとおりです。

わかりやすさ重視のコードにしていますが、再生成を考えると、本当はもう少し効率化(アロケーションしないように)すべきです。

動作確認

さて次にこのUIの描画結果を確認するために、以下のようにEditor Windowを作成します。

using UnityEditor;

public sealed class ExampleWindow : EditorWindow
{
    public void CreateGUI()
    {
        var element = new ExampleElement();
        element.style.width = 100;
        element.style.height = 100;
        rootVisualElement.Add(element);
    }

    [MenuItem("Window/Example")]
    private static void Open()
    {
        GetWindow<ExampleWindow>();
    }
}

Window > Exampleからウィンドウを開くと、下図の結果が得られることを確認できます。

結果

設定した頂点カラーが反映されたUIが描画されていることを確認できました。

テクスチャを使う場合には注意

MeshGenerationContextでテクスチャを使う場合にはUV座標の設定に注意が必要です。
テクスチャは効率化のため、内部的にアトラス化される場合があるので、UV座標にはアトラスのUVを考慮した値を設定する必要があります。

これは以下のコメント部分のように、MeshGenerationContext.uvRegionを使って行います。

using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleElement : VisualElement
{
    private readonly Texture2D _texture;

    public ExampleElement()
    {
        generateVisualContent += OnGenerateVisualContent;

        // 適当にテクスチャを作る
        var texture = new Texture2D(2, 2);
        _texture = texture;
        texture.SetPixels(new[]
        {
            Color.white, Color.white,
            Color.white, Color.white
        });
        texture.Apply();
    }

    private void OnGenerateVisualContent(MeshGenerationContext context)
    {
        if (contentRect.width < 0.01f || contentRect.height < 0.01f)
            return;

        var vertices = new Vertex[4];
        var indices = new ushort[] { 0, 1, 2, 2, 3, 0 };

        var mesh = context.Allocate(4, 6, _texture);

        {
            var left = 0.0f;
            var right = contentRect.width;
            var top = 0.0f;
            var bottom = contentRect.height;
            vertices[0].position = new Vector3(left, bottom, Vertex.nearZ);
            vertices[1].position = new Vector3(left, top, Vertex.nearZ);
            vertices[2].position = new Vector3(right, top, Vertex.nearZ);
            vertices[3].position = new Vector3(right, bottom, Vertex.nearZ);
        }

        {
            vertices[0].tint = Color.red;
            vertices[1].tint = Color.green;
            vertices[2].tint = Color.blue;
            vertices[3].tint = Color.black;
        }

        {
            indices[0] = 0;
            indices[1] = 1;
            indices[2] = 2;
            indices[3] = 2;
            indices[4] = 3;
            indices[5] = 0;
        }

        // UV座標を設定
        {
            // 内部的にテクスチャがアトラスに格納される場合があるので、UVはuvRegionを使って際スケーリングする
            Vector2 GetScaledUv(Vector2 uv)
            {
                return uv * mesh.uvRegion.size + mesh.uvRegion.min;
            }

            vertices[0].uv = GetScaledUv(new Vector2(0, 0));
            vertices[1].uv = GetScaledUv(new Vector2(0, 1));
            vertices[2].uv = GetScaledUv(new Vector2(1, 1));
            vertices[3].uv = GetScaledUv(new Vector2(1, 0));
        }

        mesh.SetAllVertices(vertices);
        mesh.SetAllIndices(indices);
    }
}

UV座標を設定する際にはこの点に注意してください。

参考

docs.unity3d.com

docs.unity3d.com