【Unity】画像をスクリプトから動的にスライスする

Unity で画像をスライスするには テスクチャーのインスペクターで、SpriteMode を Multiple に変更して SpriteEditor から Slice を指定すれば一つの画像をあらかじめ複数に分割することができますが、これだとちょっと都合が悪い時があるので、こういった事前準備をせずに実行中にスクリプトから動的に画像をスライスする方法の紹介です。

確認環境

今回の確認環境は以下の通りです。

  • Windows10
  • Unity 2020.14f1

Editor上のみで確認

実装コード

DynamicSliceTextureクラス

単純に画像を切り出すだけの場合以下のメソッドが使えます。

Sprite sp = Sprite.Create(Texture2D texture, Rect rect, Vector2 pivot...);

等間隔に縦横の個数を指定してスライスした場合左上から右下にインデックスのような番号をふってインデックスで画像を指定したりします。すると縦横の個数から切り出す位置の計算が必要になるためその操作を簡単にするためにこのメソッドを利用する形でユーティリティ化しようと思います。

以下のクラスには「Sprite Editor から切り出したときと同じような画像の取得がインデックス指定でできる処理」と、「任意の位置を切り出す処理」の2つを実装しています。また動的に画像をスライスするためのクラスです。テクスチャー自体は編集しないので ScriptableObject で実装しています。

// Odin導入していれば各所コメントインしてもいいかも
//using Sirenix.OdinInspector;
using UnityEngine;

/// <summary>
/// テクスチャーを動的にスライスするためのスクリプト
/// </summary>
[CreateAssetMenu(menuName = "ScriptableObjects/Sample/SlicedTextures")]
public class DynamicSliceTexture : ScriptableObject
{
    //
    // Const
    // - - - - - - - - - - - - - - - - - - - -

    // Pivot Center用
    private static readonly Vector2 center = new Vector2(0.5f, 0.5f);
    // 5桁のゼロ埋め
    private const string numFormat = "D5";

    //
    // Inspectors
    // - - - - - - - - - - - - - - - - - - - -

    // 対象テクスチャーを縦横に等分割する

    [SerializeField/*, LabelText("対象テクスチャ")*/] Texture2D sourceTexture;
    [SerializeField/*, MinValue(1), LabelText("横のフレーム数")*/] int columns = 1;
    [SerializeField/*, MinValue(1), LabelText("縦のフレーム数")*/] int rows = 1;
    [SerializeField] float pixelsPerUnit = 100;
    // 1枚当たりの画像の大きさ
    [SerializeField/*, ReadOnly*/] int width;
    [SerializeField/*, ReadOnly*/] int height;
    [SerializeField/*, ReadOnly*/] int imageCount = -1;

    //
    // Props
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 要素数を取得します。
    /// </summary>
    public int ImageCount => this.imageCount;

    //
    // Runtime impl
    // - - - - - - - - - - - - - - - - - - - -

    public void OnValidate()
    {
        // 1枚当たりの画像サイズ
        this.width = 0;
        this.height = 0;
        if (this.sourceTexture != null)
        {
            this.width = this.sourceTexture.width / this.columns;
            this.height = this.sourceTexture.height / this.rows;
        }

        this.updateImageCount();
    }

    private void updateImageCount() => this.imageCount = this.columns * this.rows;

    //
    // Public Methods
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 指定したインデックスの画像取得します。
    /// </summary>
    /// <remarks>
    /// インデックスは左上から右下に振られる
    /// e.g.
    /// > 0 1 2 3
    /// > 4 5 6 7
    /// > 8 9...
    /// 
    /// 注意:
    /// 動的に生成したSpriteは使用が終了したらDestryを呼ばないとリークする
    /// Sprite sp;
    /// sp.sprite = Getsprite(0);
    /// // こうすると直接代入すると古いSpriteはシーンに残る(=リークしているように見える)
    /// 
    /// Sprite old = sp.sprite;
    /// Destry(old):
    /// sp.sprite = Getsprite(0); // 先に破棄してから入れ替えること
    /// </remarks>
    public Sprite GetSprite(int index)
    {
        // 左下が原点(0,0)なのでそのように計算する

        int _index = index;
        if (index > this.imageCount)
        {
            _index = index % this.imageCount;
        }

        var (x, y) = this.getIndexXY(_index);
        float xpos = x * this.width;
        float ypos = (this.rows - y - 1) * this.height;
        var rect = new Rect(xpos, ypos, this.width, this.height);
        //var sp = Sprite.Create(this.sourceTexture, rect, center);
        var sp = 
            Sprite.Create(this.sourceTexture, rect, center, 
                this.pixelsPerUnit, 0, SpriteMeshType.FullRect);
        sp.name = _index.ToString(numFormat);

        return sp;
    }

    /// <summary>
    /// 元画像から任意の位置を切り出して Sprite を取得します。
    /// </summary>
    public Sprite GetSprite(int x, int y, int width, int height, 
        float pixelsPerUnit = 100, uint extrude = 0,
            SpriteMeshType meshType = SpriteMeshType.FullRect)
    {
        Rect rect = new Rect(x, y, width, height);
        return Sprite.Create(this.sourceTexture, rect, center, 
            pixelsPerUnit, extrude, meshType);
    }

    //
    // Non-Public Methods
    // - - - - - - - - - - - - - - - - - - - -

    private (int x, int y) getIndexXY(int i)
    {
        int x = (i % this.columns);
        int y = i / this.rows;
        return (x, y);
    }
}

作った画像は特にキャッシュなどはしていないですが、もし最適化するとなったら一度生成したものはキャッシュするほうが有利な時もあるかと思います。

使い方

使い方ですが上記が ScriptableObject なのでまず以下のようにフィールドを設定してインスペクターからオブジェクトを設定しておきます。

public class DynamicSliceTextureTest : MonoBehaviour
{
    // 表示テクスチャー(スライス)
    [SerializeField] DynamicSliceTexture textureSource;

で画像の取得方法は以下のようにするとすべて取得できます。

// 左上から右下に切り出した画像を全部列挙する
int count = this.textureSource.ImageCount;
for (int i = 0; i < count; i++)
{
    Sprite _sp = this.textureSource.GetSprite(i);
    Destroy(_sp);
}

// 任意の位置を切り出す
Sprite _sp = this.textureSource.GetSprite(100, 100, 200, 200);
Destroy(_sp);

1点注意ですが、ここで取得した Sprite は使い終わったら自分で Destroy しない限り解放されないので Sprite.sprite へ設定する前に元の画像を開放しておかないとシーンに大量の Sprite が存在することになるので注意してください。

関連記事

ちなみに Sprite.Create が遅いのは以下記事の通り高速化できます。上記の実装にも導入済みです。

takap-tech.com