22
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

【UnityEditor】テクスチャ閲覧ツールを作ってみた

はじめに

プロジェクト内にテクスチャが増えてくると、インポート設定やデータサイズに問題が無いかのチェックを行うのが大変になってきます。
そこでプロジェクト内に存在するテクスチャを一か所でまとめて見れるようなツールを作成してみたところ、とても便利だったのでご紹介したいと思います。

作ったもの

テクスチャの情報を1か所にまとめてみることができます。
image.png

GitHubにて公開中です(MITライセンス)
https://github.com/rngtm/UnityEditor-TextureViewer

ソートできる

大きすぎるファイルや設定に漏れがあるファイルを一目で見つけることができます。
1b.gif

環境

Unity2018.4.3f1

ツールの実装について

今回はTreeViewを利用してツールを実装しました。

TreeViewの参考リンク

TreeViewの実装については以下のブログを参考にさせていただきました。
http://light11.hatenadiary.com/entry/2019/02/07/010146

Unity公式リンク
https://docs.unity3d.com/Manual/TreeViewAPI.html

TreeViewのソースコード(UnityCsReference)
https://github.com/Unity-Technologies/UnityCsReference/tree/master/Editor/Mono/GUI/TreeView

実装 解説

実装を全て載せると長くなってしまうので、要所をかい摘んで解説いたします。

【Tips 1】データの定義

今回は以下の情報を表示させるようにしてみました。
・ テクスチャについての情報(テクスチャの名前や解像度など)
・ テクスチャインポート設定

上記を表現するクラスを以下のように定義しました。

TextureTreeElement.cs

/** ********************************************************************************
 * @summary TreeViewで表示するデータ
 ***********************************************************************************/
public class TextureTreeElement
{
    private ulong textureByteLength = 0; // テクスチャファイルのバイト長
    private string textureDataSizeText = ""; // textureByteLengthを読みやすくテキストで表現したもの
    public string AssetPath { get; set; } // 背景アセットパス
    public string AssetName { get; set; } // 背景アセット名
    public ulong TextureByteLength => textureByteLength; // テクスチャデータサイズ(Byte)
    public string TextureDataSizeText => textureDataSizeText;// テクスチャデータサイズテキスト
    public Texture2D Texture { get; set; } // ロードしたテクスチャ 
    public TextureImporter TextureImporter { get; set; } // テクスチャインポート設定
    public int Index { get; set; } // 何番目の要素か(TreeViewで表示する際に使う)
    public TextureTreeElement Parent { get; private set; } // 親の要素(TreeViewで表示する際に使う)
    public List<TextureTreeElement> Children { get; } = new List<TextureTreeElement>(); // 子の要素(TreeViewで表示する際に使う)

    /** ********************************************************************************
    * @summary データサイズ更新
    ***********************************************************************************/
    public void UpdateDataSize()
    {
        textureByteLength = (ulong)Texture.GetRawTextureData().Length;
        textureDataSizeText = UIUtils.ConvertToHumanReadableSize(textureByteLength);
    }

    /** ********************************************************************************
    * @summary 子を追加
    ***********************************************************************************/
    internal void AddChild(TextureTreeElement child)
    {
        // 既に親がいたら削除
        if (child.Parent != null)
        {
            child.Parent.RemoveChild(child);
        }

        // 親子関係を設定
        Children.Add(child);
        child.Parent = this;
    }

    /** ********************************************************************************
    * @summary 子を削除
    ***********************************************************************************/
    public void RemoveChild(TextureTreeElement child)
    {
        if (Children.Contains(child))
        {
            Children.Remove(child);
            child.Parent = null;
        }
    }
}

【Tips 2】表の見た目のTreeViewを作る

MultiColumnHeaderを利用するコンストラクタを使うことで、表のような見た目のTreeViewが作れるようになります。

public partial class TextureTreeView : TreeView
{
    /** ********************************************************************************
    * @summary コンストラクタ
    ***********************************************************************************/
    public TextureTreeView(TreeViewState state) : base(new TreeViewState(), new TextureColumnHeader(new MultiColumnHeaderState(headerColumns)))
    {
        showAlternatingRowBackgrounds = true; // 背景のシマシマを表示
        showBorder = true; // 境界線を表示

        multiColumnHeader.sortingChanged += OnSortingChanged; // ソート変化時の処理を登録
    }

【Tips 3】ヘッダーの実装

image.png

列を定義する

ヘッダー列を定義するには MultiColumnHeaderStateMultiColumnHeaderState.Column[] を渡してやります。

TextureTreeView.cs

    // TreeViewのヘッダーの列の定義
    static readonly TextureColumn[] headerColumns = new[] {
        new TextureColumn("Texture", 170f), // 0
        new TextureColumn("Texture Type", 105f), // 1
        new TextureColumn("Non Power of 2", 105f), // 2
        new TextureColumn("Max Size", 70f), // 3
        new TextureColumn("Generate\nMip Maps", 70f), // 4
        new TextureColumn("Alpha is\nTransparency", 96f), // 5
        new TextureColumn("Texture Size", 105f), // 6
        new TextureColumn("Data Size", 80f), // 7
    };

    /** ********************************************************************************
    * @summary コンストラクタ
    ***********************************************************************************/
    public TextureTreeView(TreeViewState state) 
    : base(new TreeViewState(), new TextureColumnHeader(new MultiColumnHeaderState(headerColumns)))
    {

...(以下省略)



MultiColumnHeaderState のコンストラクタ引数には MultiColumnHeaderState.Column の派生クラス を渡しています。

TextureColumn.cs
/** ********************************************************************************
* @summary MultiColumnHeaderState.Columnの派生クラス
***********************************************************************************/
public class TextureColumn : MultiColumnHeaderState.Column
{
    /** ********************************************************************************
    * @summary コンストラクタ
    * @param   label : 列のヘッダー文字
    * @param   width : 列の横幅(pixel)
    ***********************************************************************************/
    public TextureColumn(string label, float width) : base()
    {
        base.width = width;
        autoResize = false; // 横幅が勝手に変わらないようにする
        headerContent = new GUIContent(label); 
    }
}

ヘッダーの大きさを変える

ヘッダーの大きさを変えたい場合はMultiColumnHeaderheight に数値を代入します。

今回は派生クラスを作成し、コンストラクタにてheaderに数値を代入しました。

TextureColumnHeader.cs
/** ********************************************************************************
* @summary MultiColumnHeaderの派生クラス
***********************************************************************************/
public class TextureColumnHeader : MultiColumnHeader
{
    public static readonly float headerHeight = 36f;

    /** ********************************************************************************
    * @summary コンストラクタ
    ***********************************************************************************/
    public TextureColumnHeader(MultiColumnHeaderState state) : base(state)
    {
        height = headerHeight; // ヘッダーの高さ 上書き
    }

ヘッダーの文字を下ぞろえにする

MultiColumnHeader クラスの ColumnHeaderGUI() メソッドをオーバーライドし、ラベルを下ぞろえにしてみました。

実装にはUnityCsReferenceにあるMultiColumnHeader.csを参考にしました。

TextureColumnHeader.cs

     static readonly float labelY = 4f; // ラベル位置
     private GUIStyle style;

     /** ********************************************************************************
     * @summary TreeViewのヘッダー描画
     ***********************************************************************************/
     protected override void ColumnHeaderGUI(MultiColumnHeaderState.Column column, Rect headerRect, int columnIndex)
     {
         if (canSort && column.canSort)
         {
             SortingButton(column, headerRect, columnIndex);
         }

         if (style == null)
         {
             style = new GUIStyle(DefaultStyles.columnHeader);
             style.alignment = TextAnchor.LowerLeft; // 下ぞろえにする
         }

         float labelHeight = headerHeight;
         Rect labelRect = new Rect(headerRect.x, headerRect.yMax - labelHeight - labelY, headerRect.width, labelHeight);
         GUI.Label(labelRect, column.headerContent, style);
     }

【Tips 4】BuildRootメソッドの実装

TreeView を実装するには BuildRootBuildRows メソッドを実装する必要があります。

TreeView.BuildRoot() ではTreeViewの最も根本に位置するアイテムを作成する処理を実装します。
ここで作成するルートアイテムはTreeView上では表示されません。

TextureTreeView.cs
/** ********************************************************************************
* @summary ルートとなる要素を作成
***********************************************************************************/
protected override TreeViewItem BuildRoot()
{
    // BuildRootではRootだけを作成して返す
    return new TextureTreeViewItem { id = -1, depth = -1, displayName = "Root" };
}

【Tips 5】BuildRowsメソッドの実装

TreeView.BuildRows() では表の列を作成する処理を実装します。

TextureTreeView.cs

private static readonly TreeViewItem DummyTreeViewItem 
    = new TreeViewItem { id = -999, depth = 0, displayName = "なし" };
private static readonly List<TreeViewItem> DummyTreeViewList 
    = new List<TreeViewItem> { DummyTreeViewItem };

 /** ********************************************************************************
 * @summary 列の作成
 * @note    BuildRows()で返されたIListを元にしてTreeView上で描画が実行されます。
 ***********************************************************************************/
 protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
 {
     var rows = base.BuildRows(root); // TreeView.BuildRows()の内部では検索による絞り込みを行っている
     if (hasSearch && rows.Count == 0) // 検索ヒットなしの場合
     {
         return DummyTreeViewList; // ダミーのデータを返しておく(リストの要素にnullが存在する場合、エラーが発生するので注意)
     }

     // TreeViewItemの親子関係を構築
     var elements = new List<TreeViewItem>();

     CustomUI.RowCount = baseElements.Count();
     foreach (var baseElement in baseElements)
     {
         var baseItem = CreateTreeViewItem(baseElement) as TextureTreeViewItem;
         baseItem.data = baseElement; // ソートに利用するデータを設定

         root.AddChild(baseItem); // ルートに追加
         rows.Add(baseItem); // 列に追加
     }

     // 親子関係に基づいてDepthを自動設定するメソッド
     SetupDepthsFromParentsAndChildren(root);

     return rows;
 }
TextureTreeViewItem.cs
/** ********************************************************************************
* @summary TreeViewItemの派生クラス
***********************************************************************************/
public class TextureTreeViewItem : TreeViewItem
{
    public TextureTreeElement data { get; set; }
}

補足: GetRows()を使うと検索が無効になる

BuildRows()の中で列データを取得する際に TreeView.GetRows() を使うと検索による絞り込みが無効になってしまうので注意してください。

TreeView.BuildRows() を使うと検索の結果を得ることができます。

TextureTreeView.cs
var rows = base.BuildRows(root); // TreeView.BuildRows()の内部では検索による絞り込みを行っている



ちなみに、TreeView.BuildRows() の中身の実装はUnityCsReferenceのTreeViewControl.csから見ることができます。

TreeViewControl.cs

    // Default implementation of BuildRows assumes full tree was built in BuildRoot. With full tree we can also support search out of the box
    protected virtual IList<TreeViewItem> BuildRows(TreeViewItem root)
    {
        // Reuse cached list (for capacity)
        if (m_DefaultRows == null)
            m_DefaultRows = new List<TreeViewItem>(100);
        m_DefaultRows.Clear()      

        if (hasSearch)
            m_DataSource.SearchFullTree(searchString, m_DefaultRows);
        else
            AddExpandedRows(root, m_DefaultRows);
        return m_DefaultRows;
    }

【Tips 6】全ての行の描画処理

RowGUIのオーバーライドを実装することで、TreeViewの描画処理を実装することができます。

TextureTreeView.cs

private Texture2D iconTexture = null; // Prefabアイコン

 /** ********************************************************************************
  * @summary TreeViewの列の描画
  ***********************************************************************************/
 protected override void RowGUI(RowGUIArgs args)
 {
     if (iconTexture == null)
     {
         // Prefabアイコンをロード
         iconTexture = EditorGUIUtility.Load("Prefab Icon") as Texture2D;
     }

     // TreeViewの各列の描画
     for (var visibleColumnIndex = 0; visibleColumnIndex < args.GetNumVisibleColumns(); visibleColumnIndex++)
     {
         var rect = args.GetCellRect(visibleColumnIndex); // 描画範囲を取得
         var columnIndex = args.GetColumn(visibleColumnIndex); // 列のインデックス取得(最左列の場合は0を取得)
         var labelStyle = args.selected ? EditorStyles.whiteLabel : EditorStyles.label;
         labelStyle.alignment = fieldLabelAnchor; // テキストを左揃えにする

         DrawRowColumn(args, rect, columnIndex); // 列を描画
     }
 }

【Tips 7】行1つの描画処理

RowGUIArgs.GetColumn() メソッドを利用することで、今描画しようとしている列が何番目かを知ることができます。
今回はswitch文で分岐させ、対応する描画処理を記述しました。

テキスト表示には EditorGUI.LabelField() を利用しています。

TextureTreeView.cs

private const int MB = 1024 * 1024; // メガバイト
private const int yellowDataSize = 2 * MB; // データサイズがこれを超えたら黄色で警告
private const int redDataSize = 3 * MB; // データサイズがこれを超えたら赤で警告
private const int yellowTextureSize = 2048; // テクスチャサイズがこれを超えたら黄色で警告
private const int redTextureSize = 4096; // テクスチャサイズがこれを超えたら黄色で警告
private const int redMaxTextureSize = 2048; // テクスチャ最大サイズがこれを超えたら赤で警告

/** ********************************************************************************
* @summary 列の行を描画
***********************************************************************************/
private void DrawRowColumn(RowGUIArgs args, Rect rect, int columnIndex)
{
    if (args.item.id < 0) { return; }  // 検索がヒットしない場合はid=-999のダミー(DummyTreeViewItem)が入ってくる。ここでは描画をスキップする

    TextureTreeElement element = baseElements[args.item.id];

    var texture = element.Texture;
    if (element.Texture == null) { return; }
    if (element.TextureImporter == null) { return; }

    GUIStyle labelStyle = EditorStyles.label;

    switch (columnIndex)
    {
        case (int)EHeaderColumn.TextureName:
            rect.x += 2f;

            // アイコンを描画する
            Rect toggleRect = rect;
            toggleRect.y += 2f;
            toggleRect.size = new Vector2(12f, 12f);
            GUI.DrawTexture(toggleRect, texture);

            // テキストを描画する
            Rect labelRect = new Rect(rect);
            labelRect.x += toggleRect.width;
            EditorGUI.LabelField(labelRect, args.label);
            break;
        case (int)EHeaderColumn.TextureType: // TextureType            
            EditorGUI.LabelField(rect, element.TextureImporter.textureType.ToString()); 
            break;
        case (int)EHeaderColumn.NPot: // Non power of 2
            if (element.TextureImporter.npotScale == TextureImporterNPOTScale.None)
            {
                labelStyle = MyStyle.RedLabel;
            }
            EditorGUI.LabelField(rect, element.TextureImporter.npotScale.ToString(), labelStyle);
            break;
        case (int)EHeaderColumn.MaxSize: // Max size
            if (element.TextureImporter.maxTextureSize > redMaxTextureSize)
            {
                labelStyle = MyStyle.RedLabel;
            }
            EditorGUI.LabelField(rect, element.TextureImporter.maxTextureSize.ToString(), labelStyle);
            break;
        case (int)EHeaderColumn.GenerateMips: // Generate mip maps
            if (element.TextureImporter.mipmapEnabled == true)
            {
                labelStyle = MyStyle.RedLabel;
            }
            EditorGUI.LabelField(rect, element.TextureImporter.mipmapEnabled.ToString(), labelStyle);
            break;
        case (int)EHeaderColumn.AlphaIsTransparency: // Alpha is Transparency
            EditorGUI.LabelField(rect, element.TextureImporter.alphaIsTransparency.ToString());
            break;
        case (int)EHeaderColumn.TextureSize: // Texture Size
            switch ((element.Texture.width, element.Texture.height))
            {
                case var values when values.width > redTextureSize || values.height > redTextureSize:
                    labelStyle = MyStyle.RedLabel;
                    break;
                case var values when values.width > yellowTextureSize || values.height > yellowTextureSize:
                    labelStyle = MyStyle.YellowLabel;
                    break;
            }
            EditorGUI.LabelField(rect, $"{element.Texture.width}x{element.Texture.height}", labelStyle);
            break;
        case (int)EHeaderColumn.DataSize: // データサイズ
            switch ((int)element.TextureByteLength)
            {
                case int len when len > redDataSize:
                    labelStyle = MyStyle.RedLabel;
                    break;
                case int len when len > yellowDataSize:
                    labelStyle = MyStyle.YellowLabel;
                    break;
                default:
                    break;
            }
            EditorGUI.LabelField(rect, element.TextureDataSizeText, labelStyle);
            break;
    }
}
MyStyle.cs
using UnityEngine;
using UnityEditor;

/** ********************************************************************************
* @summary GUIStyleなどの定義
***********************************************************************************/
public static class MyStyle
{
    public static GUIStyle YellowLabel { get; private set; } // 黄色いラベル
    public static GUIStyle RedLabel { get; private set; } // 赤いラベル

    /** ********************************************************************************
    * @summary GUIStyleが無ければ作成
    ***********************************************************************************/
    public static void CreateGUIStyleIfNull()
    {
        if (YellowLabel == null)
        {
            YellowLabel = new GUIStyle(EditorStyles.label);
            YellowLabel.normal.textColor = Color.yellow;
        }

        if (RedLabel == null)
        {
            RedLabel = new GUIStyle(EditorStyles.label);
            RedLabel.normal.textColor = new Color(1f, 0.1f, 0f);
        }
    }
}

【Tips 8】検索の実装

TreeView.searchStringに検索文字列を設定すると勝手に絞り込んでくれます。

検索絞り込みの実装
treeView.searchString = searchField.OnToolbarGUI(treeView.searchString, GUILayout.MaxWidth(280f));

今回は検索文字列が変化したときにだけTreeView.searchStringに代入するようにしてみました。

TextureViewerWindow.cs
EditorGUI.BeginChangeCheck();
searchText = searchField?.OnToolbarGUI(searchText, GUILayout.MaxWidth(280f));
if (EditorGUI.EndChangeCheck())
{
    if (treeView != null)
    {
        // TreeView.searchStringに検索文字列を入れると表示Itemを絞ってくれる
        treeView.searchString = searchText;
    }
}

【Tips 9】ソートの実装

ソートに関しての実装は複雑なので、今回はソースコードだけ載せます。

multiColumnHeader.sortingChangedにソート変化メソッドを登録

multiColumnHeader.sortingChanged にメソッドを登録することで、ヘッダーをクリックした際にメソッドが実行されるようになります。

image.png

TextureTreeView.cs

        /** ********************************************************************************
        * @summary コンストラクタ
        ***********************************************************************************/
        public TextureTreeView(TreeViewState state)
            //: base(state, new TextureColumnHeader(new MultiColumnHeaderState(headerColumns)))
        : base(new TreeViewState(), new TextureColumnHeader(new MultiColumnHeaderState(headerColumns)))
        {
            showAlternatingRowBackgrounds = true; // 背景のシマシマを表示
            showBorder = true; // 境界線を表示

            multiColumnHeader.sortingChanged += OnSortingChanged; // ソート変化時の処理を登録
        }

TreeViewのサンプルを参考にソート処理を実装してみました。

公式リファレンスのページにあるTreeViewExamples.zip がサンプルデータになります。

TextureTreeView_Sort.cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;

public partial class TextureTreeView : TreeView
{
    // 列に対応するソート
    EHeaderColumn[] m_SortOptions =
    {
        EHeaderColumn.TextureName,
        EHeaderColumn.TextureType,
        EHeaderColumn.NPot,
        EHeaderColumn.MaxSize,
        EHeaderColumn.GenerateMips,
        EHeaderColumn.AlphaIsTransparency,
        EHeaderColumn.TextureSize,
        EHeaderColumn.DataSize,
    };

    // ソートに使用するデータの選択
    static readonly Func<TextureTreeElement, object>[] sortSelectors = new Func<TextureTreeElement, object>[]
    {
        l => l.AssetName,
        l => l.TextureImporter.textureType,
        l => l.TextureImporter.npotScale, // Non power of two
        l => l.TextureImporter.maxTextureSize, // max size
        l => l.TextureImporter.mipmapEnabled, // generate mip maps
        l => l.TextureImporter.alphaIsTransparency,
        l => l.Texture.width* l.Texture.width, // Texture Size
        l => l.TextureByteLength, // Data Size
    };

    ///** ********************************************************************************
    //* @summary Treeをリスト形式にする
    //***********************************************************************************/
    public static void TreeToList(TreeViewItem root, IList<TreeViewItem> result)
    {
        if (root == null)
            throw new NullReferenceException("root");
        if (result == null)
            throw new NullReferenceException("result");

        result.Clear();

        if (root.children == null)
            return;

        Stack<TreeViewItem> stack = new Stack<TreeViewItem>();
        for (int i = root.children.Count - 1; i >= 0; i--)
            stack.Push(root.children[i]);

        while (stack.Count > 0)
        {
            TreeViewItem current = stack.Pop();
            result.Add(current);

            if (current.hasChildren && current.children[0] != null)
            {
                for (int i = current.children.Count - 1; i >= 0; i--)
                {
                    stack.Push(current.children[i]);
                }
            }
        }
    }

    ///** ********************************************************************************
    //* @summary ソート状態の変化時に呼ばれる
    //***********************************************************************************/
    void OnSortingChanged(MultiColumnHeader multiColumnHeader)
    {
        SortIfNeeded(rootItem, GetRows());
    }

    ///** ********************************************************************************
    //* @summary ソート実行
    //***********************************************************************************/
    void SortIfNeeded(TreeViewItem root, IList<TreeViewItem> rows)
    {
        if (rows.Count <= 1)
            return;

        if (multiColumnHeader.sortedColumnIndex == -1)
        {
            return; // No column to sort for (just use the order the data are in)
        }

        // Sort the roots of the existing tree items
        SortByMultipleColumns();
        TreeToList(root, rows);
        Repaint();
    }

    ///** ********************************************************************************
    //* @summary ソート実行
    //***********************************************************************************/
    void SortByMultipleColumns()
    {
        var sortedColumns = multiColumnHeader.state.sortedColumns;

        if (sortedColumns.Length == 0)
            return;

ar myTypes = rootItem.children.Cast<TextureTreeViewItem>();
        var orderedQuery = InitialOrder(myTypes, sortedColumns);
        for (int i = 1; i < sortedColumns.Length; i++)
        {
            EHeaderColumn sortOption = m_SortOptions[sortedColumns[i]];
            bool ascending = multiColumnHeader.IsSortedAscending(sortedColumns[i]);

            var sortSelector = sortSelectors[(int)sortOption];
            orderedQuery = orderedQuery.ThenBy(l => sortSelector(l.data), ascending);

        }

        rootItem.children = orderedQuery.Cast<TreeViewItem>().ToList();
    }

    ///** ********************************************************************************
    //* @summary 初期の並び替え
    //***********************************************************************************/
    IOrderedEnumerable<TextureTreeViewItem> InitialOrder(IEnumerable<TextureTreeViewItem> elements, int[] history)
    {
        EHeaderColumn sortOption = m_SortOptions[history[0]];
        bool ascending = multiColumnHeader.IsSortedAscending(history[0]);
        var sortSelector = sortSelectors[(int)sortOption];
        return elements.Order(l => sortSelector(l.data), ascending);
    }
}

///** ********************************************************************************
//* @summary 拡張メソッド定義
//***********************************************************************************/
static class MyExtensionMethods
{
    public static IOrderedEnumerable<T> Order<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector, bool ascending)
    {
        if (ascending) // 昇順
        {
            return source.OrderBy(selector);
        }
        else // 降順
        {
            return source.OrderByDescending(selector);
        }
    }

    public static IOrderedEnumerable<T> ThenBy<T, TKey>(this IOrderedEnumerable<T> source, Func<T, TKey> selector, bool ascending)
    {
        if (ascending) // 昇順
        {
            return source.ThenBy(selector);
        }
        else // 降順
        {
            return source.ThenByDescending(selector);
        }
    }
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
22
Help us understand the problem. What are the problem?