Version: 2022.3
言語: 日本語
さまざまなスプライトアトラスのシナリオの解決
スプライトパッカーモード

スプライトパッカー

スプライトパッカーの非推奨化

Sprite Packer (スプライトパッカー) は、Unity 2020.1 以降では非推奨となっており、スプライトパッカーモード のオプションとして利用できなくなります。すでに スプライトパッカー を使用している既存のプロジェクトでは引き続き使用できますが、2020.1 以降に作成された新規プロジェクトでは、テクスチャをパックする際に スプライトアトラス システムがデフォルトとなります。

はじめに

スプライトのグラフィックスをデザインする場合、キャラクター毎にテクスチャファイルを分けた方が作業をしやすくなります。ただし、スプライトテクスチャには、グラフィック要素の間に空白部分が多く、ランタイムに無駄なビデオメモリを消費する結果になります。パフォーマンスを最適化するためには、いくつかのスプライトテクスチャからのグラフィックスをアトラスと呼ばれる 1 つのテクスチャに隙間をあまり空けずひとまとめにするのが最善です。Unity は、個々のスプライトテクスチャからアトラスを自動的に生成するユーティリティ、スプライトパッカー を提供します。

Unity がスプライトからテクスチャアトラスの生成と処理を行うため、ユーザーは手動で処理を行う必要はありません。アトラスは必要に応じて再生モードへの移行やビルド時にパック可能であり、スプライトオブジェクトのグラフィックスがアトラスから生成され、取得できます。

ユーザーはテクスチャインポーターで Packing Tag (パッキングタグ) を指定して、そのテクスチャのスプライトのパッキングを有効にする必要があります。

スプライトパッカーの使い方

スプライトパッカーはデフォルトで無効になっていますが、エディター 設定 (Edit -> Project Settings -> Editor) で制御できます。スプライトのパックモードは Disabled から、Enabled for Builds (パックはビルドにのみ使い、再生モードには使えません)、または、Always Enabled (再生モードとビルド両方にパックが使えます) に変更できます。

Sprite Packer ウィンドウ (Window > 2D > Sprite Packer) を開き、左上隅の Pack ボタンをクリックすると、アトラスにパックされたテクスチャが表示されます。

Project ウィンドウでスプライトを選択すると、アトラス内の位置を示すためにハイライトされます。アウトラインは実際に描画されるメッシュのアウトラインで、同時に Tight packing (タイトパック、隙間があまりないパック) に使用される領域を定義します。

Sprite Packer ウィンドウ上部にあるツールバーには、パックと表示のためのコントロールがいくつか有ります。Pack ボタンを押すとパッキング操作を開始しますが、アトラスが最後にパッキングされてから変更が無ければ、何も更新されません (下記の スプライトパッカーのカスタマイズ で説明していますが、カスタムパックポリシーを実装している場合は Repack ボタンが表示されます)。メニューの View AtlasPage # では、どのアトラスのどのページをウインドウに表示するか、選択することができます (最大テクスチャサイズにすべてのスプライトを格納するための十分なスペースがない場合は、1 つのアトラスが複数のページに分割される場合があります)。ページ番号の横にあるドロップダウンで、アトラスの “パッキングポリシー” を選択できます (後述)。ツールバーの右にある 2 つのコントロールでは、ビューをズームしたり、表示するアトラスのカラーとアルファの表示を切り替えることができます。

パックポリシー

スプライトパッカーがアトラスにスプライトをどのように割り当てるかを決めるために、 パッキングポリシー を使用します。独自のパッキングポリシーを作成することも可能ですが (下記参照)、 Default Packer PolicyTight Packer PolicyTight Rotate Enabled Sprite Packer Policy オプションがすでに用意されているので、いつでも使う事ができます。これらのポリシーでは テクスチャインポーターPacking Tag プロパティからスプライトをパックするアトラスの名前を直接選択します。したがって、同じパッキングタグを持つスプライトは全て、同じアトラスに梱包されます。その後、アトラスはソーステクスチャの設定と一致するように、テクスチャのインポート設定によりソートされます。テクスチャ圧縮設定が同じスプライトは、可能であれば、同じアトラスにまとめられます。

  • Packing Tag で “[TIGHT]” (隙間無し) が指定されていない場合は、DefaultPackerPolicy は、デフォルトで矩形のパックを使用します (つまり、パッキングタグを “[TIGHT]Character” に設定すると、間隔を詰めてパックします)。
  • TightPackerPolicy は、スプライトが隙間のないメッシュを持っている場合、デフォルトで隙間なしパッキングを使用します。Packing Tag で “[RECT]” が指定されている場合は、矩形パックが行われます (つまり、Packing Tag を “[RECT]UI_Elements” に設定すると、強制的に矩形パックになります)。
  • TightRotateEnabledSpritePackerPolicy は、スプライトが隙間のないメッシュを持っている場合、デフォルトで隙間なしパッキングを使用し、スプライトを回転させることができます。Packing Tag で “[RECT]” を指定すると、矩形パッキングが行われます (つまり、Packing Tag を “[RECT]UI_Elements” に設定すると、強制的に矩形パックになります)。

スプライトパッカーのカスタマイズ

たいていは DefaultPackerPolicy オプションで十分ですが、必要に応じて独自のカスタムパックポリシーを実装することができます。そのためには、エディタースクリプトのクラスに UnityEditor.Sprites.IPackerPolicy インターフェースを実装する必要があります。このインターフェースには次のメソッドが必要です。

  • GetVersion - パックポリシーのバージョン値を返します。ポリシースクリプトを変更するとバージョンは 1 つ上がり、バージョン管理にこのポリシーが保存されます。
  • OnGroupAtlases - ここにパックロジックを実装します。PackerJob のアトラスを定義し、指定した TextureImporter からスプライトを割り当てます。

DefaultPackerPolicy はデフォルトでは矩形パック (SpritePackingMode を参照) を使用します。これは、テクスチャ空間のエフェクトを行なったり、スプライトをレンダリングするために別のメッシュを使用したい場合に便利です。カスタムポリシーはこれをオーバーライドし、代わりに Tight Packing を使用できます。

  • Repack ボタンはカスタムポリシーが選択された場合のみ有効です。
    • OnGroupAtlases は TextureImporter のメタデータや選択している PackerPolicy のバージョンが変更されないかぎり呼び出されません。
    • カスタムポリシーで作業する際は Repack ボタンを使用してください。
  • スプライトは TightRotateEnabledSpritePackerPolicy によって自動的に回転させてパックすることができます。
    • SpritePackingRotation 型は将来の Unity バージョンのために予約されています。

その他

  • アトラスは Project\Library\AtlasCache にキャッシュされてます。
    • このフォルダーを削除して Unity を起動すると、Unity はアトラスを強制的に再パックします。フォルダーの削除をする場合は必ず Unity を閉じてから行うようにしてください。
  • アトラスキャッシュは起動時に読み込みません。
    • すべてのテクスチャは Unity が再起動した後、最初にパックする時にチェックされます。この処理はプロジェクト内のテクスチャの総数により時間がかかる場合があります。
    • 必要なアトラスだけが読み込まれます。
  • デフォルトの最大アトラスサイズは 2048x2048 です。
  • PackingTag が設定されている場合、テクスチャは圧縮されないため、スプライトパッカーは元のピクセル値を取得し、次にアトラスで圧縮を行います。

DefaultPackerPolicy

using System;
using System.Linq;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;


public class DefaultPackerPolicySample : UnityEditor.Sprites.IPackerPolicy
{
        protected class Entry
        {
            public Sprite            Sprite;
        public UnityEditor.Sprites.AtlasSettings settings;
            public string            atlasName;
            public SpritePackingMode packingMode;
            public int               anisoLevel;
        }

        private const uint kDefaultPaddingPower = 3; // ベースと 2 ミップレベルに適切
        public virtual int GetVersion() { return 1; }
        protected virtual string TagPrefix { get { return "[TIGHT]"; } }
        protected virtual bool AllowTightWhenTagged { get { return true; } }
        protected virtual bool AllowRotationFlipping { get { return false; } }

    public static bool IsCompressedFormat(TextureFormat fmt)
    {
        if (fmt >= TextureFormat.DXT1 && fmt <= TextureFormat.DXT5)
            return true;
        if (fmt >= TextureFormat.DXT1Crunched && fmt <= TextureFormat.DXT5Crunched)
            return true;
        if (fmt >= TextureFormat.PVRTC_RGB2 && fmt <= TextureFormat.PVRTC_RGBA4)
            return true;
        if (fmt == TextureFormat.ETC_RGB4)
            return true;
        if (fmt >= TextureFormat.ATC_RGB4 && fmt <= TextureFormat.ATC_RGBA8)
            return true;
        if (fmt >= TextureFormat.EAC_R && fmt <= TextureFormat.EAC_RG_SIGNED)
            return true;
        if (fmt >= TextureFormat.ETC2_RGB && fmt <= TextureFormat.ETC2_RGBA8)
            return true;
        if (fmt >= TextureFormat.ASTC_RGB_4x4 && fmt <= TextureFormat.ASTC_RGBA_12x12)
            return true;
        if (fmt >= TextureFormat.DXT1Crunched && fmt <= TextureFormat.DXT5Crunched)
            return true;
        return false;
    }

    public void OnGroupAtlases(BuildTarget target, UnityEditor.Sprites.PackerJob job, int[] textureImporterInstanceIDs)
        {
            List<Entry> entries = new List<Entry>();

            foreach (int instanceID in textureImporterInstanceIDs)
            {
                TextureImporter ti = EditorUtility.InstanceIDToObject(instanceID) as TextureImporter;

                TextureFormat desiredFormat;
                ColorSpace colorSpace;
                int compressionQuality;
                ti.ReadTextureImportInstructions(target, out desiredFormat, out colorSpace, out compressionQuality);

                TextureImporterSettings tis = new TextureImporterSettings();
                ti.ReadTextureSettings(tis);

            Sprite[] Sprites =
                AssetDatabase.LoadAllAssetRepresentationsAtPath(ti.assetPath)
                    .Select(x => x as Sprite)
                    .Where(x => x != null)
                    .ToArray();
                foreach (Sprite Sprite in Sprites)
                {
                    Entry entry = new Entry();
                    entry.Sprite = Sprite;
                    entry.settings.format = desiredFormat;
                    entry.settings.colorSpace = colorSpace;
                    // Compression Format に対してのみ、後でグループ化するため Compression Quality を使用します。 それ以外の場合は空のままにします。
                entry.settings.compressionQuality = IsCompressedFormat(desiredFormat) ? compressionQuality : 0;
                entry.settings.filterMode = Enum.IsDefined(typeof(FilterMode), ti.filterMode)
                    ? ti.filterMode
                    : FilterMode.Bilinear;
                    entry.settings.maxWidth = 2048;
                    entry.settings.maxHeight = 2048;
                    entry.settings.generateMipMaps = ti.mipmapEnabled;
                    entry.settings.enableRotation = AllowRotationFlipping;
                    if (ti.mipmapEnabled)
                        entry.settings.paddingPower = kDefaultPaddingPower;
                    else
                        entry.settings.paddingPower = (uint)EditorSettings.SpritePackerPaddingPower;
                    #if ENABLE_ANDROID_ATLAS_ETC1_COMPRESSION
                        entry.settings.allowsAlphaSplitting = ti.GetAllowsAlphaSplitting ();
                    #endif //ENABLE_ANDROID_ATLAS_ETC1_COMPRESSION

                    entry.atlasName = ParseAtlasName(ti.SpritePackingTag);
                    entry.packingMode = GetPackingMode(ti.SpritePackingTag, tis.SpriteMeshType);
                    entry.anisoLevel = ti.anisoLevel;

                    entries.Add(entry);
                }

                Resources.UnloadAsset(ti);
            }

            // 最初に、スプライトをアトラス名に基づいてグループに分割します
            var atlasGroups =
                from e in entries
                group e by e.atlasName;
            foreach (var atlasGroup in atlasGroups)
            {
                int page = 0;
                // 次に、テクスチャ設定に基づいて、これらのグループを小さなグループに分割します
                var settingsGroups =
                    from t in atlasGroup
                    group t by t.settings;
                foreach (var settingsGroup in settingsGroups)
                {
                    string atlasName = atlasGroup.Key;
                    if (settingsGroups.Count() > 1)
                        atlasName += string.Format(" (Group {0})", page);

                UnityEditor.Sprites.AtlasSettings settings = settingsGroup.Key;
                    settings.anisoLevel = 1;
                    // このアトラスのすべてのエントリから最高の異方性レベルを使用します
                    if (settings.generateMipMaps)
                        foreach (Entry entry in settingsGroup)
                            if (entry.anisoLevel > settings.anisoLevel)
                                settings.anisoLevel = entry.anisoLevel;

                    job.AddAtlas(atlasName, settings);
                    foreach (Entry entry in settingsGroup)
                    {
                        job.AssignToAtlas(atlasName, entry.Sprite, entry.packingMode, SpritePackingRotation.None);
                    }

                    ++page;
                }
            }
        }

        protected bool IsTagPrefixed(string packingTag)
        {
            packingTag = packingTag.Trim();
            if (packingTag.Length < TagPrefix.Length)
                return false;
            return (packingTag.Substring(0, TagPrefix.Length) == TagPrefix);
        }

        private string ParseAtlasName(string packingTag)
        {
            string name = packingTag.Trim();
            if (IsTagPrefixed(name))
                name = name.Substring(TagPrefix.Length).Trim();
            return (name.Length == 0) ? "(unnamed)" : name;
        }

        private SpritePackingMode GetPackingMode(string packingTag, SpriteMeshType meshType)
        {
            if (meshType == SpriteMeshType.Tight)
                if (IsTagPrefixed(packingTag) == AllowTightWhenTagged)
                    return SpritePackingMode.Tight;
            return SpritePackingMode.Rectangle;
        }
}

TightPackerPolicy

using System;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Sprites;
using System.Collections.Generic;

// パックタグが [RECT] を含まないかぎり、TightPackerPolicy は矩形でないスプライトを隙間なくパックします。
class TightPackerPolicySample : DefaultPackerPolicySample
{
        protected override string TagPrefix { get { return "[RECT]"; } }
        protected override bool AllowTightWhenTagged { get { return false; } }
        protected override bool AllowRotationFlipping { get { return false; } }
}

TightRotateEnabledSpritePackerPolicy

```` using System; using System.Linq; using UnityEngine; using UnityEditor; using UnityEditor.Sprites; using System.Collections.Generic;

// パックタグが [RECT] を含まないかぎり、TightPackerPolicy は矩形でないスプライトを隙間なくパックします。 class TightRotateEnabledSpritePackerPolicySample : DefaultPackerPolicySample { protected override string TagPrefix { get { return “[RECT]”; } } protected override bool AllowTightWhenTagged { get { return false; } } protected override bool AllowRotationFlipping { get { return true; } } }


  • Sprite Packer は 2020.1 で非推奨NewIn20191
さまざまなスプライトアトラスのシナリオの解決
スプライトパッカーモード