[Unity] [Editor拡張] Crunch圧縮を一括で適用するEditorWindowの作成方法

四回目の投稿です。よろしくお願いします。



# 目次
1. [概要](#概要)
2. [使用方法](#使用方法)
3. [作業手順](#作業手順)
4. [実装方法](#実装方法)
5. [最後に](#最後に)



# 概要
以下のような、指定した圧縮率でテクスチャをCrunch圧縮するEditorWindowの作成方法を解説します : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/b50bffa8-e349-c574-8fb5-c40093af1265.png)

:::note info
Crunch圧縮の設定は、個々のテクスチャのインスペクター上でも可能です : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/6915f33d-9bf0-53fe-16e9-f74c4de56393.png)
:::

:::note info
Crunch圧縮について
Crunch圧縮は圧縮形式のことで、解凍が早く、ロード時間にほとんど影響を与えないため、アプリ容量が削減できるということで、公式が積極的な使用を推奨しています。
:::


# 使用方法
まずは、外観を把握するという意味でも、使用方法を説明します。作成方法に興味のない方は、ここを読むことで使用できるようになっています。

##### 1. 下記スクリプトを作成し、"Editor"フォルダ内に入れる : 
<details><summary>TextureCrunchCompressTool.cs (tap)</summary>

```
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class TextureCrunchCompressTool : EditorWindow
{
    private const int MinCompressionRate = 0;
    private const int MaxCompressionRate = 100;
    
    private static readonly string[] TextureExtensions = new string[] { ".png", ".jpg" }; // 対象としたい画像の拡張子

    private readonly List<Texture> compressedTextures = new List<Texture>(); // 圧縮した画像たち
    private Vector2 scrollValueForDrawCrunchCompressedGUI;
    [Range(MinCompressionRate, MaxCompressionRate)]
    private int currentCompressionRate = 50;
    private bool isCompressed;
    private bool isOpenFoldoutForDrawCrunchCompressedGUI;

    [MenuItem("Tools/Texture Crunch Compress Tool")]
    public static void OpenEditorWindow()
    {
        GetWindow<TextureCrunchCompressTool>("テクスチャの圧縮ツール");
    }

    private void OnEnable()
    {
        Initialize();
    }

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Initialize()
    {
        compressedTextures.Clear();
        scrollValueForDrawCrunchCompressedGUI = Vector2.zero;
        isCompressed = false;
        isOpenFoldoutForDrawCrunchCompressedGUI = false;
    }

    private void OnGUI()
    {
        DrawCrunchCompressionSettingGUI();

        GUILayout.Space(5);

        if (isCompressed) // 圧縮を実行するまで表示しない
        {
            DrawCrunchCompressedGUI();
        }
    }

    /// <summary>
    /// 圧縮に関する設定画面の描画
    /// </summary>
    private void DrawCrunchCompressionSettingGUI()
    {
        GUILayout.Label("[概要]\n" +
                        "テクスチャを指定した圧縮率に圧縮します。");

        GUILayout.Space(5);

        currentCompressionRate = EditorGUILayout.IntSlider("圧縮率 : ", currentCompressionRate, MinCompressionRate, MaxCompressionRate);

        GUILayout.Space(5);

        if (GUILayout.Button("Crunch圧縮を実行"))
        {
            Initialize();
            ExecuteCrunchCompress();
            isCompressed = true;
        }
    }

    /// <summary>
    /// Crunch圧縮を実行
    /// </summary>
    private void ExecuteCrunchCompress()
    {
        var paths = GetAllTexturePath("Assets"); // "Assets"下にある画像をすべて取得
        foreach (var path in paths)
        {
            var impoter = (TextureImporter)AssetImporter.GetAtPath(path); // 画像設定を変更するためにAssetImporterを使用
            if (impoter.crunchedCompression && impoter.compressionQuality == currentCompressionRate) { continue; } // 未圧縮なら、圧縮する

            var texture = (Texture)AssetDatabase.LoadAssetAtPath(path, typeof(Texture)); // テクスチャの取得
            compressedTextures.Add(texture);

            impoter.crunchedCompression = true;                  // use crunch compressを有効化
            impoter.compressionQuality = currentCompressionRate; // 圧縮率の設定
            AssetDatabase.ImportAsset(path);                     // 変更を適用
        }
    }

    /// <summary>
    /// 特定のフォルダ下にあるTextureのパスをすべて取得 (検索 -> guid取得 -> パス取得)
    /// </summary>
    private static List<string> GetAllTexturePath(string rootFolder)
    {
        var guids = AssetDatabase.FindAssets("t:Texture", new string[] { rootFolder }); // "rootFolder"フォルダ下の全てのTextureのGUID
        var assetPaths = new List<string>();
        foreach (var guid in guids)
        {
            var path = AssetDatabase.GUIDToAssetPath(guid); // guid -> path
            var isSubject = false;
            foreach (var extension in TextureExtensions)
            {
                if (!path.Contains(extension)) { continue; } // 画像の拡張子が、対象となるものであるか?

                isSubject = true;
            }
            if (!isSubject) { continue; }

            assetPaths.Add(path);
        }
        return assetPaths;
    }

    /// <summary>
    /// 圧縮した画像の一覧表示画面の描画
    /// </summary>
    private void DrawCrunchCompressedGUI()
    {
        if (compressedTextures.Count < 1)
        {
            GUILayout.Label("圧縮対象は存在しませんでした。");
            return;
        }
        GUILayout.Label($"以下の対象に圧縮処理({currentCompressionRate}%)が適用されました : ");

        isOpenFoldoutForDrawCrunchCompressedGUI = EditorGUILayout.Foldout(isOpenFoldoutForDrawCrunchCompressedGUI, $"対象一覧 [{compressedTextures.Count}]");
        if (isOpenFoldoutForDrawCrunchCompressedGUI) // 折り畳みが開かれているなら、
        {
            scrollValueForDrawCrunchCompressedGUI = EditorGUILayout.BeginScrollView(scrollValueForDrawCrunchCompressedGUI);
            foreach (var compressedTexture in compressedTextures) // すべての圧縮した画像に対し、
            {
                EditorGUILayout.ObjectField(compressedTexture, typeof(Texture), false); // 表示
            }
            EditorGUILayout.EndScrollView();
        }
    }
}
#endif
```
</details>

:::note info
Editorフォルダについて
Unityにはフォルダ名によって識別される特殊なフォルダが存在する。その内の一つにEditorフォルダがある。そして、エディタースクリプトを"Editor"と名付けたフォルダ内に配置しなければ、それは動作しないのである。
(エディタースクリプト : UnityEditor名前空間に存在するクラス(EditorやEditorWindow、PropertyDrawerなど)を継承したスクリプト)

追記 : コード全体を"#if UNITY_EDITOR"と"#endif"で囲っている場合は、エディタフォルダに入れていなくても良い。
:::


##### 2. Unityエディタ上で"Tools/Texture Crunch Compress Tool"を選択する : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/8e5180c9-1fe4-0e57-9552-5d7c73926902.png)


##### 3. すると、下記EditorWindowが出現するので、圧縮率を指定し、"Crunch圧縮を実行"ボタンを押すと、Assets下にあるすべてのテクスチャに対して圧縮が実行される : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/8176b471-6a10-eeb1-b98a-d12fe59d9ae0.png)


# 作業手順
#### 1. Unityエディタ上からEditorWindowを開けるように実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/8e5180c9-1fe4-0e57-9552-5d7c73926902.png)

#### 2. 圧縮実行に必要な最低限の描画処理の実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/8176b471-6a10-eeb1-b98a-d12fe59d9ae0.png)
(この時点では、まだ圧縮処理は実装されていない)

#### 3. Crunch圧縮処理の実装

#### 4. Crunch圧縮したテクスチャ表示の描画処理の実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/94da4914-f71a-34d7-dcc4-e8f070e304ff.png)



# 実装方法
#### 1. Unityエディタ上からEditorWindowを開けるように実装

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/d2ea94a2-e538-45cb-587e-4db0853cf4ba.png)

以下のように、TextureCrunchCompressTool.csを作成し、Editorフォルダ内に配置する : 

<details><summary>TextureCrunchCompressTool.cs (tap)</summary>

```
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class TextureCrunchCompressTool : EditorWindow
{
    [MenuItem("Tools/Texture Crunch Compress Tool")]
    public static void OpenEditorWindow()
    {
        GetWindow<TextureCrunchCompressTool>("テクスチャの圧縮ツール");
    }
    
    private void OnGUI() // 描画処理
    {
        
    }
}
#endif
```
</details>

:::note info
ポイント：
* #if UNITY_EDITOR : Unityエディタ上でしか動作しないようにしている。
* MenuItem属性 : Unityエディタ上のToolsに項目を追加している。
* GetWindow() : EditorWindowを開けるようにしている。
:::



#### 2. 圧縮実行に必要な最低限の描画処理の実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/a6fb21e1-44b4-612a-07bb-d8270298b8c4.png)

以下のように、TextureCrunchCompressTool.csに追記する : 
<details><summary>TextureCrunchCompressTool.cs (tap)</summary>

```diff_c_sharp
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class TextureCrunchCompressTool : EditorWindow
{
+   private const int MinCompressionRate = 0;
+   private const int MaxCompressionRate = 100;
    
+   [Range(MinCompressionRate, MaxCompressionRate)]
+   private int currentCompressionRate = 50; // 設定中の圧縮率

    [MenuItem("Tools/Texture Crunch Compress Tool")]
    public static void OpenEditorWindow()
    {
        GetWindow<TextureCrunchCompressTool>("テクスチャの圧縮ツール");
    }
    
    private void OnGUI() // 描画処理
    {
+       DrawCrunchCompressionSettingGUI();
    }

+   /// <summary>
+   /// 圧縮に関する設定画面の描画
+   /// </summary>
+   private void DrawCrunchCompressionSettingGUI()
+   {
+       GUILayout.Label("[概要]\n" +
+                       "テクスチャを指定した圧縮率に圧縮します。");
+
+       GUILayout.Space(5);
+
+       currentCompressionRate = EditorGUILayout.IntSlider("圧縮率 : ", currentCompressionRate, MinCompressionRate, MaxCompressionRate);
+       
+       GUILayout.Space(5);
+
+       if (GUILayout.Button("Crunch圧縮を実行"))
+       {
+           // Crunch圧縮を実行
+       }
+   }
}
#endif
```
</details>

:::note info
ポイント：
* Range属性 : currentCompressionRateの範囲を制限
* EditorGUILayout.IntSlider() : 圧縮率を設定するスライダーを実装
* GUILayout.Button() : 圧縮処理を実行できるようにしている。
:::



#### 3. Crunch圧縮処理の実装
以下のように、TextureCrunchCompressTool.csに追記する : 
<details><summary>TextureCrunchCompressTool.cs (tap)</summary>

```diff_c_sharp
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class TextureCrunchCompressTool : EditorWindow
{
    private const int MinCompressionRate = 0;
    private const int MaxCompressionRate = 100;
+   private static readonly string[] TextureExtensions = new string[] { ".png", ".jpg" }; // 対象としたい画像の拡張子
    
    [Range(MinCompressionRate, MaxCompressionRate)]
    private int currentCompressionRate = 50; // 設定中の圧縮率

    [MenuItem("Tools/Texture Crunch Compress Tool")]
    public static void OpenEditorWindow()
    {
        GetWindow<TextureCrunchCompressTool>("テクスチャの圧縮ツール");
    }
    
    private void OnGUI() // 描画処理
    {
        DrawCrunchCompressionSettingGUI();
    }

    /// <summary>
    /// 圧縮に関する設定画面の描画
    /// </summary>
    private void DrawCrunchCompressionSettingGUI()
    {
        GUILayout.Label("[概要]\n" +
                        "テクスチャを指定した圧縮率に圧縮します。");
 
        GUILayout.Space(5);
 
        currentCompressionRate = EditorGUILayout.IntSlider("圧縮率 : ", currentCompressionRate, MinCompressionRate, MaxCompressionRate);
      
        GUILayout.Space(5);
 
        if (GUILayout.Button("Crunch圧縮を実行"))
        {
+           ExecuteCrunchCompress();
        }
    }

+   /// <summary>
+   /// Crunch圧縮を実行
+   /// </summary>
+   private void ExecuteCrunchCompress()
+   {
+       var paths = GetAllTexturePath("Assets"); // "Assets"下にある画像をすべて取得
+       foreach (var path in paths)
+       {
+           var impoter = (TextureImporter)AssetImporter.GetAtPath(path); // 画像設定を変更するためにAssetImporterを使用
+           if (impoter.crunchedCompression && impoter.compressionQuality == currentCompressionRate) { continue; } // 未圧縮なら、圧縮する
+
+           impoter.crunchedCompression = true;                  // use crunch compressを有効化
+           impoter.compressionQuality = currentCompressionRate; // 圧縮率の設定
+           AssetDatabase.ImportAsset(path);                     // 変更を適用
+       }
+   }
+
+   /// <summary>
+   /// 特定のフォルダ下にあるTextureのパスをすべて取得 (検索 -> guid取得 -> パス取得)
+   /// </summary>
+   private static List<string> GetAllTexturePath(string rootFolder)
+   {
+       var guids = AssetDatabase.FindAssets("t:Texture", new string[] { rootFolder }); // "rootFolder"フォルダ下の全てのTextureのGUID
+       var assetPaths = new List<string>();
+       foreach (var guid in guids)
+       {
+           var path = AssetDatabase.GUIDToAssetPath(guid); // guid -> path
+           var isSubject = false;
+           foreach (var extension in TextureExtensions)
+           {
+               if (!path.Contains(extension)) { continue; } // 画像の拡張子が、対象となるものであるか?
+
+               isSubject = true;
+           }
+           if (!isSubject) { continue; }
+
+           assetPaths.Add(path);
+       }
+       return assetPaths;
+   }
}
#endif
```
</details>

:::note info
ポイント：
* GetAllTexturePath("Assets") : Assets下に存在するすべてのテクスチャのパスを取得している。(例えば、引数を"Assets/Resources/Texture"にすると、そのフォルダより下にあるものにしか適用されなくなる)
* AssetDatabase.FindAssets() : filterを"t:Texture"とすることで、すべてのテスクチャを検索している。(Unityエディタ上のProjectウィンドウの検索欄に"t:Texture"と入力すると、すべてのテクスチャが表示される)
* TextureExtensions変数 : 拡張子を用いて適用範囲を制限している。
* アセットインポーターを用いることで、圧縮の設定を変更できるようにしている。
:::

"Crunch圧縮を実行"ボタンを押し、適当なテクスチャを選択して、インスペクターを確認し、以下のようになっていれば成功 : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/fd9c1a8d-43c7-44a3-f154-cfe283d7cb84.png)


この時点で、テクスチャの圧縮の実装は完了しているが、利便性向上のため、圧縮が適用されたテクスチャを表示する描画処理を、以下で実装する。

#### 4. Crunch圧縮したテクスチャ表示の描画処理の実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/94da4914-f71a-34d7-dcc4-e8f070e304ff.png)
以下のように、TextureCrunchCompressTool.csに追記する : 
<details><summary>TextureCrunchCompressTool.cs (tap)</summary>

```diff_c_sharp
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class TextureCrunchCompressTool : EditorWindow
{
    private const int MinCompressionRate = 0;
    private const int MaxCompressionRate = 100;
    private static readonly string[] TextureExtensions = new string[] { ".png", ".jpg" }; // 対象としたい画像の拡張子

+   private readonly List<Texture> compressedTextures = new List<Texture>(); // 圧縮した画像たち
+   private Vector2 scrollValueForDrawCrunchCompressedGUI;
    [Range(MinCompressionRate, MaxCompressionRate)]
    private int currentCompressionRate = 50; // 設定中の圧縮率
+   private bool isCompressed;
+   private bool isOpenFoldoutForDrawCrunchCompressedGUI;

    [MenuItem("Tools/Texture Crunch Compress Tool")]
    public static void OpenEditorWindow()
    {
        GetWindow<TextureCrunchCompressTool>("テクスチャの圧縮ツール");
    }

+   private void OnEnable()
+   {
+       Initialize();
+   }
+
+   /// <summary>
+   /// 初期化処理
+   /// </summary>
+   private void Initialize()
+   {
+       compressedTextures.Clear();
+       scrollValueForDrawCrunchCompressedGUI = Vector2.zero;
+       isCompressed = false;
+       isOpenFoldoutForDrawCrunchCompressedGUI = false;
+   }
    
    private void OnGUI() // 描画処理
    {
        DrawCrunchCompressionSettingGUI();
+
+       GUILayout.Space(5);
+
+       if (isCompressed) // 圧縮を実行するまで表示しない
+       {
+           DrawCrunchCompressedGUI();
+       }
    }

    /// <summary>
    /// 圧縮に関する設定画面の描画
    /// </summary>
    private void DrawCrunchCompressionSettingGUI()
    {
        GUILayout.Label("[概要]\n" +
                        "テクスチャを指定した圧縮率に圧縮します。");
 
        GUILayout.Space(5);
 
        currentCompressionRate = EditorGUILayout.IntSlider("圧縮率 : ", currentCompressionRate, MinCompressionRate, MaxCompressionRate);
      
        GUILayout.Space(5);
 
        if (GUILayout.Button("Crunch圧縮を実行"))
        {
+           Initialize();
            ExecuteCrunchCompress();
+           isCompressed = true;
        }
    }

    /// <summary>
    /// Crunch圧縮を実行
    /// </summary>
    private void ExecuteCrunchCompress()
    {
        var paths = GetAllTexturePath("Assets"); // "Assets"下にある画像をすべて取得
        foreach (var path in paths)
        {
            var impoter = (TextureImporter)AssetImporter.GetAtPath(path); // 画像設定を変更するためにAssetImporterを使用
            if (impoter.crunchedCompression && impoter.compressionQuality == currentCompressionRate) { continue; } // 未圧縮なら、圧縮する
 
            impoter.crunchedCompression = true;                  // use crunch compressを有効化
            impoter.compressionQuality = currentCompressionRate; // 圧縮率の設定
            AssetDatabase.ImportAsset(path);                     // 変更を適用
        }
    }
 
    /// <summary>
    /// 特定のフォルダ下にあるTextureのパスをすべて取得 (検索 -> guid取得 -> パス取得)
    /// </summary>
    private static List<string> GetAllTexturePath(string rootFolder)
    {
        var guids = AssetDatabase.FindAssets("t:Texture", new string[] { rootFolder }); // "rootFolder"フォルダ下の全てのTextureのGUID
        var assetPaths = new List<string>();
        foreach (var guid in guids)
        {
            var path = AssetDatabase.GUIDToAssetPath(guid); // guid -> path
            var isSubject = false;
            foreach (var extension in TextureExtensions)
            {
                if (!path.Contains(extension)) { continue; } // 画像の拡張子が、対象となるものであるか?
 
                isSubject = true;
            }
            if (!isSubject) { continue; }
 
            assetPaths.Add(path);
        }
        return assetPaths;
    }
+
+   /// <summary>
+   /// 圧縮した画像の一覧表示画面の描画
+   /// </summary>
+   private void DrawCrunchCompressedGUI()
+   {
+       if (compressedTextures.Count < 1)
+       {
+           GUILayout.Label("圧縮対象は存在しませんでした。");
+           return;
+       }
+       GUILayout.Label($"以下の対象に圧縮処理({currentCompressionRate}%)が適用されました : ");
+
+       isOpenFoldoutForDrawCrunchCompressedGUI = EditorGUILayout.Foldout(isOpenFoldoutForDrawCrunchCompressedGUI, $"対象一覧 [{compressedTextures.Count}]");
+       if (isOpenFoldoutForDrawCrunchCompressedGUI) // 折り畳みが開かれているなら、
+       {
+           scrollValueForDrawCrunchCompressedGUI = EditorGUILayout.BeginScrollView(scrollValueForDrawCrunchCompressedGUI);
+           foreach (var compressedTexture in compressedTextures) // すべての圧縮した画像に対し、
+           {
+               EditorGUILayout.ObjectField(compressedTexture, typeof(Texture), false); // 表示
+           }
+           EditorGUILayout.EndScrollView();
+       }
+   }
}
#endif
```
</details>

:::note info
ポイント：
* isCompressed変数 : 圧縮処理が実行されるまで、結果を表示しないようにしている。
* EditorGUILayout.Foldout() : 対象一覧の表示を折りたためるようにしている。
* EditorGUILayout.BeginScrollView()とEditorGUILayout.EndScrollView() : 対象一覧をスクロールできるようにしている。
* EditorGUILayout.ObjectField() : 対象を表示している。
:::



# 最後に
どうでしたか、わかりやすかったでしょうか。Crunch圧縮の設定自体は、個々のテクスチャのインスペクター上で設定できるが、個数が多いとかなり面倒なので、こういったエディタ拡張を作成できる能力は有用です。特に、ベイク時に生成されるライトマップは、基本的に個数が多いので、一つずつ適用していくのは骨が折れます。
また気が向いたら何か書きます。それでは。

宣伝 : CUPLEXという時間差アクションパズルゲームを作成しています。私は主にプログラムとプロジェクト管理を担当しています。よかったら、覗いてみてください↓
https://store.steampowered.com/app/2499100/CUPLEX/