Programming Serendipity

プログラミングを中心に種々雑多に書き留めます

uGUIでラスタースクロール

↓こんなのをつくります。続きからどうぞ

f:id:q7z:20170523231623g:plain (画像はFurball 2Dからお借りしました)

なにはともあれスクリプトをご覧ください。 これをUIRasterScroll.csとして保存してImageにアタッチするだけで使えます。

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

/// <summary>
/// uGUIでラスタースクロール
/// </summary>
[RequireComponent(typeof(Canvas))]
[RequireComponent(typeof(Image))]
public class UIRasterScroll : BaseMeshEffect 
{
    /// <summary>Xの分割数</summary>
    [SerializeField]
    private int divX = 1;

    /// <summary>Yの分割数</summary>
    [SerializeField]
    private int divY = 32;

    /// <summary>波の幅</summary>
    [SerializeField]
    private float range = 124f;

    /// <summary>波の数</summary>
    [SerializeField]
    private float frequency = 0.24f;

    /// <summary>波打つ速度</summary>
    [SerializeField]
    private float speed = 8f;


    /// <summary>対象コンポーネント</summary>
    private Image img;
    private Image Img { get { return img != null ? img : (img = GetComponent<Image>()); } }

    /// <summary>左下の頂点</summary>
    private UIVertex leftBottom;

    /// <summary>右上の頂点</summary>
    private UIVertex rightTop;

    /// <summary>色情報</summary>
    private Color color;

    /// <summary>元の頂点格納リスト</summary>
    private List<UIVertex> verts = new List<UIVertex>();

    /// <summary>新しく計算された頂点</summary>
    private List<UIVertex> newVerts = new List<UIVertex>();

    /// <summary>計算用頂点</summary>
    private List<UIVertex> calcVerts = new List<UIVertex>();


    /// <summary>Mono method</summary>
    void Update()
    {
        // 毎フレーム強制的に再計算
        color = Img.color;
        Img.SetAllDirty();
    }

    /// <summary>
    /// 再計算されるときに呼ばれるメソッド
    /// </summary>
    /// <param name="vh"></param>
    public override void ModifyMesh(VertexHelper vh)
    {
        if (!IsActive())
            return;

        verts.Clear();
        vh.GetUIVertexStream(verts);

        verts = GetRasterizedVertices(verts);

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

    /// <summary>
    /// ラスタライズされた頂点を取得
    /// </summary>
    /// <param name="verts">元の頂点リスト</param>
    /// <returns>新しい頂点リスト</returns>
    public List<UIVertex> GetRasterizedVertices(List<UIVertex> verts)
    {
        // 6頂点であることを想定
        Debug.AssertFormat(verts.Count == 6, "assumed verts.Count == 6, but {0}.", verts.Count);

        // 左下が[0]、右上が[2]と[3]であると仮定する
        // 内部仕様の変更などで違っていたら、左下と右上に相当するインデックスを渡す
        leftBottom = verts[0];
        rightTop = verts[2];

        // まずは格子点上の情報を保存
        calcVerts.Clear();
        calcVerts.Capacity = (divX + 1) * (divY + 1);
        for (int i = 0; i <= divY; i++)
        {
            for (int j = 0; j <= divX; j++)
            {
                calcVerts.Add(GetDividedVertex(j, i));
            }
        }

        // そこから取り出す
        newVerts.Clear();
        newVerts.Capacity = divX * divY * 6;
        for (int i = 0; i < divY; i++)
        {
            for (int j = 0; j < divX; j++)
            {
                newVerts.Add(calcVerts[(i + 0) * (divX + 1) + j + 0]);
                newVerts.Add(calcVerts[(i + 1) * (divX + 1) + j + 0]);
                newVerts.Add(calcVerts[(i + 1) * (divX + 1) + j + 1]);
                newVerts.Add(calcVerts[(i + 1) * (divX + 1) + j + 1]);
                newVerts.Add(calcVerts[(i + 0) * (divX + 1) + j + 1]);
                newVerts.Add(calcVerts[(i + 0) * (divX + 1) + j + 0]);
            }
        }

        return newVerts;
    }

    /// <summary>
    /// 場所に応じた頂点情報を取得
    /// </summary>
    /// <param name="indexX">Xのインデックス</param>
    /// <param name="indexY">Yのインデックス</param>
    /// <returns>インデックスに応じた頂点情報</returns>
    UIVertex GetDividedVertex(int indexX, int indexY)
    {
        Debug.Assert(divX != 0);
        Debug.Assert(divY != 0);
        Debug.Assert(indexX <= divX);
        Debug.Assert(indexY <= divY);
        Debug.Assert(indexX >= 0);
        Debug.Assert(indexY >= 0);

        var tmp = UIVertex.simpleVert;
        var x = leftBottom.position.x + (rightTop.position.x - leftBottom.position.x) * indexX / divX + Mathf.Sin(Time.time * speed + indexY * frequency) * range;
        var y = leftBottom.position.y + (rightTop.position.y - leftBottom.position.y) * indexY / divY;
        var u = (float)indexX / divX;
        var v = (float)indexY / divY;

        tmp.position = new Vector2(x, y);
        tmp.uv0 = new Vector2(u, v);
        tmp.color = color;

        return tmp;
    }
}

もろもろ

  • BaseMeshEffectを継承することで頂点情報がいじれるようになります。
  • BaseMeshEffectMonoBehaviourを継承しているので、そのままコンポーネントとしてアタッチできます
  • ImageSetAllDirty()で再計算が必要であることを通知します。これによって描画処理の段階?でModifyMesh()が呼ばれます
  • ModifyMeshのメソッド名とかシグネチャがUnity5.2前後でかなり頻繁に仕様変更されていたようですがさすがにもう確定ですよね…?(Unity 5.6.0f3で確認)
  • 現在のUnityではImage1枚が6頂点のようです。 f:id:q7z:20170523231641p:plain
  • UIVertexのxyz, uv, rgbaなどに自分で頂点情報を構築して、その頂点のリストをVertexHelper.AddUIVertexTriangleStream()に渡すことでカスタム描画できるようです。
  • 大半の頂点は重複するので、格子点だけ計算して再利用
  • UIVertex.simpleVertがデフォ値的存在なので、それを変更して使っていく(normalとかtangentとかの面倒を見たくない)
  • UIVertexのcolorフィールドは、強制的に白にしたりするとコンポーネント側の設定を上書きしてしまうので、コンポーネントの値を取得して反映
  • Imgで??を使っていないのはUnityEngine.Objectのnullとの判定が==!=のときのみオーバーロードされていて??のときにされていない(できない)ため

たのしい!やったぜ。BaseMeshEffectと戯れているとOpenGLをいじっている気分になりますが、実際それくらい低レイヤーな処理ですからね。使いこなすと色々愉快なことができそうです。TypeFace AnimatorとかText Mesh PROとかもこれで実装してるんですかね?