[Unity] Spriteを範囲指定付きテクスチャとして使う

# 経緯
![texture_animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/9085c991-606b-6585-3ad2-542301e9c97d.gif)
一枚の画像に異なるパーツの画像が入った**画像の範囲を指定してテクスチャとして使用**したい。どうせならUnity上で **[SpriteEditor](https://docs.unity3d.com/ja/2018.2/Manual/SpriteEditor.html)** を使って視覚的に範囲指定できると便利。
つまり**Spriteをスプライト以外のテクスチャとしても使えれば便利**！

以前書いた記事 [【Unity】 クォータービューのドット絵に深度バッファを適用する](https://qiita.com/Shinoda_Naoki/items/fccdc137747ea4301578) でスプライトを3Dモデルに投影する手法を試しましたが、この時 Texture には Sprite 用のものではなく通常のタイプを使っていました。今回は、スプライトのままで3Dオブジェクトに貼り付ける方法を試してみました。

その過程で、通常の**3Dモデル用マテリアルのテクスチャにスプライトを使う方法**もわかったのでまとめてみます。

# Sprite から Texture 範囲を取得（マテリアル汎用）
ググってみたら、わざわざ[ピクセルを書き出す例](https://answers.unity.com/questions/651984/convert-sprite-image-to-texture.html)があったけど、そんな面倒なことしなくても、今回の目的のためには [Sprite.texture](https://docs.unity3d.com/ja/2017.4/ScriptReference/Sprite-texture.html) と [Sprite.textureRect](https://docs.unity3d.com/ja/2017.4/ScriptReference/Sprite-textureRect.html) がわかれば十分でした。

## マテリアルのテクスチャに範囲を設定する
スプライトの元のテクスチャと範囲矩形がわかるので、それをマテリアルに設定してやります。
[Material.mainTexture](https://docs.unity3d.com/ja/2017.4/ScriptReference/Material-mainTexture.html)でテクスチャをセットし、[Material.mainTextureOffset](https://docs.unity3d.com/ja/2017.4/ScriptReference/Material-mainTextureOffset.html)と[Material.mainTextureScale](https://docs.unity3d.com/ja/2017.4/ScriptReference/Material-mainTextureScale.html)で範囲指定します。

```csharp:SpriteAnime.cs
        var renderer = GetComponent<MeshRenderer>();
        var material = renderer.material;
        var texSize = new Vector2(sprite.texture.width, sprite.texture.height);
        var rect = sprite.textureRect;
        material.mainTexture = sprite.texture;
        material.mainTextureOffset = new Vector2(rect.x/ texSize.x, rect.y / texSize.y);
        material.mainTextureScale = new Vector2(rect.width / texSize.x, rect.height / texSize.y);
```

公式ドキュメントにも書いてありますが、上で使用した Material のプロパティはシェーダーの `_MainTex` を置き換えます。最後の三行はこう書いても同じです。

```csharp
        material.SetTexture("_MainTex",sprite.texture);
        material.SetTextureOffset("_MainTex", new Vector2(rect.x, rect.y));
        material.SetTextureScale("_MainTex", new Vector2(1 / rect.width, 1 / rect.height));
```
メインテクスチャ以外にスプライトを使いたい場合は、上記のように名前指定で置き換えられるでしょう。

## ついでにスプライトアニメーションやってみた
ただスプライトを貼るだけでは芸がないので、スプライトの配列を設定してアニメーションができるようにしてみました。

```csharp:SpriteAnime.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpriteAnime : MonoBehaviour
{
    [SerializeField, Header("アニメーションリスト")]
    private Sprite[] sprites = new Sprite[0];

    [SerializeField, Header("アニメーション速度"),Range(1,100)]
    private float speed = 50f;

    // Update is called once per frame
    void Update()
    {
        if (sprites.Length < 1) return;

        var index = (int)Mathf.Repeat(Time.frameCount * speed / 100, sprites.Length);
        var renderer = GetComponent<MeshRenderer>();
        var material = renderer.material;

        var sprite = sprites[index];
        var texSize = new Vector2(sprite.texture.width, sprite.texture.height);
        var rect = sprite.textureRect;
        material.mainTexture = sprite.texture;
        material.mainTextureOffset = new Vector2(rect.x/ texSize.x, rect.y / texSize.y);
        material.mainTextureScale = new Vector2(rect.width / texSize.x, rect.height / texSize.y);
        material.SetTexture("_MainTex",sprite.texture);
        material.SetTextureOffset("_MainTex", new Vector2(rect.x, rect.y));
        material.SetTextureScale("_MainTex", new Vector2(1 / rect.width, 1 / rect.height));

        renderer.material = material;
    }
}
```

上記をコンポーネントとして適当なゲームオブジェクトに追加します。
![spriteAnime.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/209e5548-0472-429d-f907-f7b4db0dd30c.jpeg)![spriteeditor.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/edf561a9-6e39-56ab-8053-1f1c3548eee6.jpeg)

### 結果
![texture_animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/9085c991-606b-6585-3ad2-542301e9c97d.gif)

# Shedクラスで使う
一般的なゲームオブジェクトに対する設定方法がわかったので、今度は Shed クラス（[以前の記事](https://qiita.com/Shinoda_Naoki/items/fccdc137747ea4301578)で作ったドット絵を直方体メッシュに正射影するクラスです）でやってみます。
![finalResult.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/61b0eff2-5822-bfe0-5a3f-24085a6b896e.jpeg)
もともとクラス内で mesh に UV座標を設定していたので、そこを変えるだけでよいです。
以下は関連する部分だけ抜き出したものです。完全なソースはこちら（[GitHub](https://github.com/ShinodaNaoki/isometricPixelarts/blob/support_sprite_textures/Assets/Assets/Scripts/Shed4Sprite.cs)）

```csharp:Shed4Sprite.cs

    public Sprite sprite;

    // ドット絵を保持するマテリアル
    public Material material;

    // マテリアルのメインテクスチャサイズ
    private Vector2Int texSize;

    // Spriteのテクスチャ領域
    private RectInt spriteRect;
    void Start()
    {
        if (!needRestruct) return;
        needRestruct = true;
        heplMessage = null;

        if (fieldsNotReady()) return;
        spriteRect = GetSpriteRect();
        texSize = GetTextureSize();

        Mesh mesh = InitializeCube();
        var newMaterial = Instantiate(material);
        newMaterial.SetTexture("_MainTex", sprite.texture);
        GetComponent<MeshFilter>().sharedMesh = mesh;
        GetComponent<MeshRenderer>().material = newMaterial;
    }

    /// <summary>
    /// テクスチャサイズを取得
    /// </summary>
    /// <returns></returns>
    private Vector2Int GetTextureSize()
    {
        var tex = sprite.texture;

        return new Vector2Int(tex.width, tex.height);
    }

    /// <summary>
    /// Spriteのテクスチャ範囲を取得
    /// </summary>
    /// <returns></returns>
    private RectInt GetSpriteRect()
    {
        var rect = sprite.textureRect;

        return new RectInt((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
    }
    /// <summary>
    /// pivotからの相対位置をUV座標に変換する
    /// </summary>
    /// <param name="offestX"></param>
    /// <param name="offsetY"></param>
    /// <returns></returns>
    private Vector2 ToUV(float offestX, float offsetY)
    {
        var x = pivot.x + offestX + spriteRect.x;
        var y = pivot.y + offsetY + spriteRect.y;
        var pos = new Vector2( x / texSize.x, y / texSize.y);
        //Debug.LogFormat("ToUV:({0},{1})",pos.x, pos.y);
        return pos;
    }
```

## 疑似3Dサイズを自動計算させよう
Shed クラスはドット絵の疑似3Dサイズを指定するようになっていますが、スプライトの画像サイズが無駄な余白を含んでいないと仮定すれば、pivot.xと画像サイズから以下のように導き出せます。

```csharp
       var width = size.x + size.z;
       var height = size.y + width / 2);
```

せっかく SpriteEditor で視覚的に範囲指定できるので、計算で求められるものは計算して、手入力しなければならないパラメータは極力減らしたいですね。また関連部分だけの抜き出しですが、以下のようにしてみました。

```csharp:Shed4Sprite.cs
    [SerializeField, Header("スプライトとpivot.xからsize自動計算")]
    public bool autoSizeAdjust = true;

    // マテリアルのメインテクスチャサイズ
    private Vector2Int texSize;

    // Spriteのテクスチャ領域
    private RectInt spriteRect;

    // Inspector 表示用の警告メッセージ
    public string heplMessage;

    private void doAutoSizeAdjust()
    {
        Debug.LogFormat("Texture size:({0},{1})", texSize.x, texSize.y);
        Debug.LogFormat("Sprite rect:({0},{1})-({2},{3})", spriteRect.x, spriteRect.y, spriteRect.width, spriteRect.height);
        pivot.y = 0;
        pivot.x = Mathf.Clamp(pivot.x, 0, spriteRect.width);

        size.z = pivot.x;
        size.x = spriteRect.width - pivot.x;
        size.y = spriteRect.height - spriteRect.width / 2;

        if (size.y < 0)
        {
            heplMessage = "スプライトの高さが足りません。最低でも横幅の半分以上必要です。";
            size.y = 0;
        }
    }

    void Start()
    {
        if (!needRestruct) return;
        needRestruct = true;
        heplMessage = null;

        if (fieldsNotReady()) return;
        spriteRect = GetSpriteRect();
        texSize = GetTextureSize();

        if (autoSizeAdjust)
        {
            doAutoSizeAdjust();
        }
        else if (!verifySize())
        {
            heplMessage = "スプライトのサイズは指定の3Dサイズに必要な大きさがありません";
        }

        //... 中略 ... //
    }

    private bool verifySize()
    {
        var width = size.x + size.z;
        if (spriteRect.width < width) return false;
        return spriteRect.height >= size.y + width / 2;
    }
```

## プレビューにこだわる
これで、面倒な座標入力は pivot.x だけになりました。ここまで来たらこれも [WYSIWYG](https://ja.wikipedia.org/wiki/WYSIWYG) にしたい！ エディタ拡張でプレビューをカスタマイズして、pivot 位置を視覚的に確認できるようにしてみました。

```csharp:Shed4SpriteEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;

[CustomEditor(typeof(Shed4Sprite))]
public class Shed4SpriteEditor : Editor
{
    Shed4Sprite shed = null;
    SerializedProperty sprite;
    SerializedProperty material;
    SerializedProperty pivot;
    SerializedProperty size;
    SerializedProperty autoAdjust;

    void OnEnable()
    {
        shed = target as Shed4Sprite;
        sprite = serializedObject.FindProperty("sprite");
        material = serializedObject.FindProperty("material");
        pivot = serializedObject.FindProperty("pivot");
        size = serializedObject.FindProperty("size");
        autoAdjust = serializedObject.FindProperty("autoSizeAdjust");
    }

    public override void OnInspectorGUI()
    {
        //　シリアライズオブジェクトの更新
        serializedObject.Update();

        EditorGUI.BeginDisabledGroup(true);
        EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((MonoBehaviour)target), typeof(MonoScript), false);
        EditorGUI.EndDisabledGroup();

        EditorGUI.BeginChangeCheck();

        EditorGUILayout.PropertyField(pivot);
        EditorGUILayout.PropertyField(sprite);
        EditorGUILayout.PropertyField(material);

        EditorGUILayout.PropertyField(autoAdjust);
        if (autoAdjust.boolValue)
        {
            EditorGUI.BeginDisabledGroup(true);
            EditorGUILayout.PropertyField(size);
            EditorGUI.EndDisabledGroup();
        }
        else
        {
            EditorGUILayout.PropertyField(size);
        }
        //　シリアライズオブジェクトのプロパティの変更を更新
        serializedObject.ApplyModifiedProperties();

        string help = shed.heplMessage;
        if (help != null && help.Length > 0)
        {
            EditorGUILayout.HelpBox(help, MessageType.Warning);
        }

        if (EditorGUI.EndChangeCheck())
        {
            shed.UpdateMesh();
        }
    }

    // プレビューウィンドウを表示するかどうか
    public override bool HasPreviewGUI()
    {
        return true;
    }

    private bool ZoomAroundPivot
    {
        get { return shed.zoomAroundPivot; }
        set { shed.zoomAroundPivot = value; }
    }

    // プレビューウィンドウのヘッダーバーをカスタムする関数
    public override void OnPreviewSettings()
    {
        ZoomAroundPivot = GUILayout.Toggle(ZoomAroundPivot, "zoom pivot");
    }

    // プレビューウィンドウで描画させたいものはここで書く
    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        if (ZoomAroundPivot) _drawAroundPivot(r);
        else _drawEntire(r);
    }

    private void _drawEntire(Rect r)
    {
        Vector2 size2D = shed.Size2D;

        const int border = 2;

        var scale = Mathf.Min((r.width - border * 2)/ size2D.x , (r.height - border * 2)/ size2D.y);
        var centerPos = new Vector2(r.x + r.width / 2, r.y + r.height / 2);
        var drawSize = size2D * scale;
        var size3D = ((Vector3)shed.size) * scale;

        var offsetPos = new Vector2(centerPos.x - border - drawSize.x / 2, centerPos.y -border - drawSize.y / 2);

        // 3D投影サイズ境界のx,zを赤と緑の外殻線で描く
        var maxPos = new Vector2(offsetPos.x + drawSize.x + border * 2, offsetPos.y + drawSize.y + border * 2);
        Handles.DrawSolidRectangleWithOutline(new Rect(offsetPos.x, offsetPos.y, size3D.x + border, border - 1), Color.red, Color.red);
        Handles.DrawSolidRectangleWithOutline(new Rect(offsetPos.x, offsetPos.y, border - 1, size3D.x / 2 + border), Color.red, Color.red);
        Handles.DrawSolidRectangleWithOutline(new Rect(offsetPos.x, maxPos.y, size3D.z + border, border - 1), Color.green, Color.green);
        Handles.DrawSolidRectangleWithOutline(new Rect(offsetPos.x, maxPos.y, border - 1, -size3D.z / 2), Color.green, Color.green);

        Handles.DrawSolidRectangleWithOutline(new Rect(maxPos.x, maxPos.y, -size3D.x - border, border - 1), Color.red, Color.red);
        Handles.DrawSolidRectangleWithOutline(new Rect(maxPos.x, maxPos.y, border - 1, -size3D.x / 2 - border), Color.red, Color.red);
        Handles.DrawSolidRectangleWithOutline(new Rect(maxPos.x, offsetPos.y, -size3D.z - border, border - 1), Color.green, Color.green);
        Handles.DrawSolidRectangleWithOutline(new Rect(maxPos.x, offsetPos.y, border - 1, size3D.z / 2 + border), Color.green, Color.green);

        var texture = shed.sprite.texture;
        var texRect = shed.SpriteRect;
        var texSize = new Vector2(texture.width, texture.height);
        //Debug.LogFormat($"({texSize.x},{texSize.y}) , ({texRect.xMin},{texRect.yMin}) -({texRect.xMax}, {texRect.yMax})");

        // 中央にスプライト領域を描画
        var drawRect = new Rect(offsetPos.x + border, offsetPos.y + border, drawSize.x, drawSize.y);
        Rect texCoord = new Rect(texRect.x / texSize.x, texRect.y / texSize.y, texRect.width / texSize.x, texRect.height / texSize.y);
        GUI.DrawTextureWithTexCoords(drawRect, texture, texCoord);
    }

    private void _drawAroundPivot(Rect r)
    {
        Vector2 size2D = shed.Size2D;

        const int border = 2;
        const int zoom = 3;

        var scale = Mathf.Min(r.width / size2D.x, (r.height - border) / size2D.y);
        scale = Mathf.Max(3, scale);
        var hfWidth = r.width / 2;
        var hfHeight = r.height / 2;

        // 3D投影サイズ境界のx,zを赤と緑の外殻線で描く
        Handles.DrawSolidRectangleWithOutline(new Rect(r.x, r.yMax, hfWidth, border - 1), Color.green, Color.green);
        Handles.DrawSolidRectangleWithOutline(new Rect(r.x + hfWidth, r.yMax, hfWidth, border - 1), Color.red, Color.red);

        var texture = shed.sprite.texture;
        var texRect = shed.SpriteRect;
        var texSize = new Vector2(texture.width, texture.height);
        //Debug.LogFormat($"({texSize.x},{texSize.y}) , ({texRect.xMin},{texRect.yMin}) -({texRect.xMax}, {texRect.yMax})");
        Vector2 pivot = shed.pivot;
        var clipSize = new Vector2(hfWidth / zoom, (r.height - border) / zoom);
        Rect texCoord = new Rect(
            (texRect.x + pivot.x - clipSize.x) / texSize.x, texRect.y / texSize.y,
            clipSize.x * 2 / texSize.x, clipSize.y / texSize.y);


        // 中央にスプライト領域を描画
        var drawRect = new Rect(r.x, r.y, r.width, r.height - border);
        GUI.DrawTextureWithTexCoords(drawRect, texture, texCoord);
    }
}
```

### 結果
pivot を変更すると連動してプレビューが動いて、赤と緑の枠がそれぞれ3D投影サイズの x,z の範囲を示すようにサムネイル画像外縁に表示します。さらに 'zoom pivot' にチェックすると pivot 周辺が拡大表示されてピクセル単位での位置あわせが容易にできるようにしました。　　
![inspector-preview.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/3c16d032-72e5-6618-ef8e-76ead00de6da.gif)

### 参考記事
エディタ拡張については下記を参考にさせていただきました。
http://baba-s.hatenablog.com/entry/2019/04/10/181000
https://techblog.kayac.com/unity_advent_calendar_2018_16
https://qiita.com/kyourikey/items/7a5f693d1fe17bde5387

# まとめ
* スプライトの texture と textureRect で元のテクスチャと引用範囲を取得できた
* マテリアルの mainTexture, mainTextureOffset, mainTextureScale で範囲指定付きでテクスチャを設定できた
* スプライトを使った3Dモデルのテクスチャアニメーションができた
* ドット絵正射影用のShedクラスもUV計算時に範囲計算を反映することでスプライトを使用できるようにした
* Shedクラスの3D投影サイズをスプライト領域から自動計算するようにした
* エディタ拡張でShedクラスのプレビューに3D投影サイズがわかりやすく表示できた