[Unity] テクスチャの圧縮フォーマットを自動設定 / 一括変換する

2020-11-21

前提知識 : テクスチャの圧縮フォーマット

ゲームに使うテクスチャ(画像)は大抵、GPU 向けに最適化された圧縮形式に変換してゲームのビルドに組み込む。 png や jpg も圧縮形式の一種ではあるが、ゲーム向けの圧縮形式はまた別で、 DXT とか ETC とか PVRTC とか ASTC といったものがある。 各圧縮形式の中でも圧縮率ごとに ASTC 4x4 とか ASTC 6x6 とか複数の形式がある。

Unity では png や jpg といったソース画像に対してインポート設定を行うことで、 プラットフォームごと / 画像ごとに任意の圧縮フォーマットを指定することができる。

今時の Unity のデフォルト設定では、iOS が PVRTC 4 bits、Android が ETC2 に設定されている。 圧縮フォーマットが対応していない古い端末では、 Unity が無圧縮状態(true color)にフォールバックして動かしてくれるようになっているが、 無圧縮状態だと VRAM の消費メモリが上がり、パフォーマンスは落ちる。

フォーマットは何を選ぶのが妥当か

プロジェクトの要件にもよるが、自分の経験としては iOS は ASTC 6x6、Android は ETC2 8bit あたりを選ぶことが多い。

iOS デフォルトの PVRTC は昔からある形式で古い端末でも動くが、 UI などのパキッとした絵では劣化が見られやすい。 後に出てきた ASTCPVRTC より優秀で、高圧縮ながら見た目の顕著な劣化が見られにくくなっている。

ASTC は A8 以降のチップを積んだ端末、すなわち iPhone 6(2014-09)以降の端末が対応している。 ETC2 の要件は OpenGL ES 3.0 以降だが、こちらは 2013 年くらいの Android から対応してるイメージだ。 2020 年の現代においては採用しても問題はないだろう。

ちなみに各フォーマットの消費メモリサイズの比較は以下:

フォーマット 256 x 256 での消費メモリ
無圧縮(RGBA 32 bit) 256 KB
ASTC 6x6 28.89 KB
ETC2 8bit 32 KB

圧縮は見た目とのトレードオフで VRAM の消費とビルドサイズを低減できる。

ちなみに ASTC 6x6 よりも低圧縮で見た目を綺麗にしたい場合は ASTC 4x4 などを選ぶ。 この 6 とか 4 の数字は圧縮を行う単位のブロックサイズのことで、数字が大きいほど高圧縮になり見た目は汚くなる。

プロジェクト内のテクスチャフォーマットをまとめて変更したい

さて、デフォルトが PVRTC だったりするので、プロジェクトがある程度成熟した段階で 「やっぱり ASTC に変えよう」などと考え直した場合に、 全テクスチャのインポート設定を手作業で変更するのは面倒だと思うだろう。

スクリプトで一括更新できるものがあると便利なのでそのようなコードを書いた:

using UnityEditor;
using UnityEngine;

namespace AltoLib
{
    /// <summary>
    /// テクスチャフォーマットを一括変換するエディタ拡張。
    /// フォーマット形式などの設定は外出ししていないので、カスタムしたい場合は
    /// スクリプトをコピーして適宜書き換えてほしい。
    /// </summary>
    public class TextureReimporter
    {
        //----------------------------------------------------------------------
        // Convert Settings
        //----------------------------------------------------------------------

        const TextureImporterFormat IosTextureFormat     = TextureImporterFormat.ASTC_6x6;
        const TextureImporterFormat AndroidTextureFormat = TextureImporterFormat.ETC2_RGBA8;
        static readonly string[] TargetFolders = {"Assets"};

        public static void SetImportSettingsForIos(TextureImporter textureImporter, string assetPath)
        {
            int originalMaxSize = textureImporter.maxTextureSize;
            textureImporter.SetPlatformTextureSettings(new TextureImporterPlatformSettings
            {
                name               = "iPhone",
                overridden         = true,
                maxTextureSize     = originalMaxSize,
                resizeAlgorithm    = TextureResizeAlgorithm.Mitchell,
                format             = IosTextureFormat,
                textureCompression = TextureImporterCompression.Compressed,
                compressionQuality = 50,
            });
            Debug.Log($"{assetPath} [iOS] : Set {IosTextureFormat.ToString()}");
        }

        public static void SetImportSettingsForAndroid(TextureImporter textureImporter, string assetPath)
        {
            int originalMaxSize = textureImporter.maxTextureSize;
            textureImporter.SetPlatformTextureSettings(new TextureImporterPlatformSettings
            {
                name               = "Android",
                overridden         = true,
                maxTextureSize     = originalMaxSize,
                resizeAlgorithm    = TextureResizeAlgorithm.Mitchell,
                format             = AndroidTextureFormat,
                textureCompression = TextureImporterCompression.Compressed,
                compressionQuality = 50,
            });
            Debug.Log($"{assetPath} [Android] : Set {AndroidTextureFormat.ToString()}");
        }

        //----------------------------------------------------------------------
        // private
        //----------------------------------------------------------------------

        const string ProgressBarTitle = "Update Texture Format";

        /// <summary>
        /// ゲームに使用されているテクスチャの iOS / Android 向け圧縮フォーマットを統一する。
        /// テクスチャの Max Size は既存の設定をそのまま引き継ぎ、圧縮フォーマットのみを指定、
        /// 変更があったものについてアセットをインポートし直す。
        /// </summary>
        [MenuItem("Alto/Convert Texture Format to ASTC or ETC2")]
        static void UpdateTextureCompressionSetting()
        {
            string[] guids = AssetDatabase.FindAssets("t:texture2D", TargetFolders);

            try
            {
                EditorUtility.DisplayProgressBar(ProgressBarTitle, "", 0f);
                for (var i = 0; i < guids.Length; ++i)
                {
                    string guid = guids[i];
                    string assetPath = AssetDatabase.GUIDToAssetPath(guid);
                    SetTextureCompression(assetPath, i, guids.Length);
                }

                EditorUtility.DisplayProgressBar(ProgressBarTitle, "Refresh AssetDatabase ...", 1f);
                AssetDatabase.Refresh();
                Debug.Log("Texture format conversion is completed.");
            }
            finally
            {
                EditorUtility.ClearProgressBar();
            }
        }

        static void SetTextureCompression(string assetPath, int count, int totalCount)
        {
            // Font テクスチャは除外
            if (assetPath.EndsWith(".ttf")) { return; }
            if (assetPath.EndsWith(".otf")) { return; }

            var textureImporter = AssetImporter.GetAtPath(assetPath) as TextureImporter;
            if (textureImporter == null)
            {
                Debug.LogError($"TextureImporter not found : {assetPath}");
                return;
            }

            if (!ShouldReimport(textureImporter)) { return; }
            // Note. プログレスバーの表示はわずかに硬直時間があるようなので実際に処理する場合だけ表示
            float progress = (float)count / totalCount;
            EditorUtility.DisplayProgressBar(ProgressBarTitle, $"{assetPath} ({count} / {totalCount})", progress);

            SetImportSettingsForIos(textureImporter, assetPath);
            SetImportSettingsForAndroid(textureImporter, assetPath);
            AssetDatabase.ImportAsset(assetPath);
        }

        static bool ShouldReimport(TextureImporter textureImporter)
        {
            var iosSettings = textureImporter.GetPlatformTextureSettings("iPhone");
            if (iosSettings.format != IosTextureFormat || iosSettings.overridden == false) { return true; }

            var androidSettings = textureImporter.GetPlatformTextureSettings("Android");
            if (androidSettings.format != AndroidTextureFormat || androidSettings.overridden == false) { return true; }

            return false;
        }
    }
}

このスクリプトを任意の場所に配置すると、メニューの AltoConvert Texture Format to ASTC or ETC2 から全テクスチャの設定の一括更新をかけられる。 (ファイルが多いと処理が終わるのに数十分とかかかるので実行の際は覚悟を決めてから)

コメントにも書いてあるが設定の外出しはしていないので、 別のフォーマットにしたい場合はスクリプト上部の圧縮設定の内容を書き換えて使ってほしい。

プロジェクトにテクスチャを足した時にデフォルト設定されてほしい

さて、プロジェクトにおけるフォーマット指定の方針が決まると、 新しく足す画像についてはデフォルトでその設定がされてほしいと思うのが人情だ。

Unity 標準でデフォルト設定ができてもよさそうなものだが、僕が知る限りはその手段はない。

そこで、アセットのプリプロセスにフックしてインポート設定を自動でセットするスクリプトを書いた:

using UnityEditor;
using UnityEngine;

namespace AltoLib
{
    /// <summary>
    /// プロジェクトにテクスチャアセットを追加した際、
    /// iOS / Android 向け圧縮フォーマットを自動で設定するサンプルコード。
    /// 各テクスチャの初回 Import 時に実行される。
    ///
    /// フォーマットは TextureReimporter で指定したものが適用される。
    /// </summary>
    public class TexturePreprocess : AssetPostprocessor
    {
        void OnPreprocessTexture()
        {
            // 初回 Import 時のみ処理を行う。
            // 以前は .meta ファイルの存在確認で判定できていたが、
            // Unity 2019.4 では Preprocess の段階で .meta ファイルが作られていた。
            // 初回は importSettingsMissing が True になるようなのでそちらで判定する。
            var textureImporter = (TextureImporter)assetImporter;
            if (!textureImporter.importSettingsMissing) { return; }

            Debug.Log($"New texture is detected : {assetPath}");
            if (textureImporter == null)
            {
                Debug.LogError($"TextureImporter not found : {assetPath}");
                return;
            }

            TextureReimporter.SetImportSettingsForIos(textureImporter, assetPath);
            TextureReimporter.SetImportSettingsForAndroid(textureImporter, assetPath);
        }
    }
}

このスクリプトを任意の場所に配置すると、新しくテクスチャを追加した時に自動でインポート設定がセットされるようになる。

この OnPreprocessTexture() というのはテクスチャの設定を変えたり Unity 上で Reimport した場合にも呼ばれるので、 そのままでは特定のテクスチャだけ個別に設定を変更したいような場合に都合が悪い。 (設定変更後にこの処理が呼ばれて再びデフォルト設定に戻ってしまう)

そこで 「ファイルをプロジェクトに新規追加した場合にだけ処理する」 よう分岐する必要があった。 この「新規追加した場合か」を判定するのは正規のやり方があるのかわからず、調査に少し時間を要した。

Unity 2019.2 の時点では飛び道具的に 「.meta ファイルが作られているか」 を見ることで判定ができていたのだが、 Unity 2019.4 ではこのプリプロセスが呼ばれる時点で .meta ファイルができてしまっていて GUID も取得できる状態になっていた。 そこで別の方法を探すとどうやら初回は TextureImporter.importSettingsMissing が True になるということがわかったので、 それを見て判定するようにした。


以上、Unity ワークフロー改善系の Tips でした。