Version: 2017.4
Sprite Editor: Edit Physics Shape
Sorting Group

Упаковщик спрайтов (Sprite Packer)

При работе со спрайтовой графикой, очень важно при работе с разными персонажами, работать с разными файлами текстур. Однако, значительное пространство спрайтовой текстуры будет уходить на пустые участки между графическими элементами, результатом чего станет бесполезный расход памяти при работе игры или приложения. Для достижения оптимальной производительности лучше всего будет упаковывать графические элементы из нескольких спрайтовых текстур как можно плотнее в одну текстуру, именуемую атласом. В Unity есть утилита Sprite Packer, предназначенная как раз для создания таких атласов из набора отдельных текстур.

В Unity процесс создания и назначения атласных текстур остаётся за кадром, позволяя пользователю сосредоточиться на более важных вещах. При желании атлас можно упаковать при входе в режим Play или во время сборки, в результате чего графическая составляющая этого спрайтового объекта отобразится сразу же, как только она будет подгружена из необходимого атласа.

Users are required to specify a Packing Tag in the Texture Importer to enable packing for Sprites of that Texture.

Использование упаковщика спрайтов

По-умолчанию упаковщик спрайтов отключён, но его работу можно настроить через настройки редактора (меню: Edit -> Project Settings -> Editor). Метод упаковывания спрайтов можно установить из состояния Disabled в состояние Enabled for Builds (где упаковывание будет работать для сборок, но не в режиме Play) или в состояние Always Enabled (где упаковывание будет работать и во время сборки и в режиме Play).

Если вы откроете окно Sprite Packer (menu: Window -> Sprite Packer) и нажмёте в левом верхнем углу на кнопку Pack, вы увидите расположение текстур упакованных на этом атласе.

Если вы выберете спрайт в панели Project, он подсветится, чтобы показать своё местоположение на атласе. Видимый контур представляет из себя контур самого меша, а также показывает область, которая будет использована для плотной упаковки.

Панель инструментов, расположенная в верхней части окна упаковщика спрайтов имеет ряд элементов управления, которые влияют на упаковывание и просмотр. Pack кнопки инициализируют операцию упаковывания, но никаких обновлений не последует до тех пор, пока не будут зарегистрированы изменения в самом атласе с момента его последней упаковки. (Если вы решите реализовать свои (пользовательские) правила упаковки, то появится соответствующая кнопка Repack, как это будет описано ниже в Customizing the Sprite Packer below). Меню View Atlas и Page # позволят вам выбрать какую страницу из какого атласа нужно отобразить в окне (если при максимальном размере текстуры в неё не помещаются все спрайты, атлас может быть разбит на множество страниц). Меню, следующее за номером страницы позволяет выбрать какие правила упаковывания используются для текущего атласа (см. ниже). Справа от панели инструментов имеются два элемента управления для зуммирования содержимого окна просмотра и переключения между цветным и альфа отображением самого атласа.

Политика упаковки

Упаковщик спрайтов использует packing policy, чтобы определять, как применять спрайты к атласам. Можно создавать и свои собственные правила упаковки (см. ниже), но опции Default Packer Policy и Tight Packer Policy будут всегда доступны. С этими правилами свойство Packing Tag в Texture Importer позволяет напрямую выделять атлас, для которого будет произведено упаковывание спрайтов по тегу. Т.е. все спрайты с таким же тегом упаковки будут упакованы в один и тот же атлас. Затем атласы дополнительно сортируются по признаку настройки импортирования текстур, чтобы даже в этом случае они совпадали несмотря на то, как прежде определил пользователь исходные текстуры. Если возможно, спрайты со схожей степенью сжатия текстур могут будут сгруппированы в одном и том же атласе.

  • DefaultPackerPolicy по-умолчанию будет использовать прямоугольный метод упаковывания до тех пор, пока не будет назначен “[TIGHT]” метод в Packing Tag (т.е. назначение методу упаковывания тега [TIGHT] приведёт к использованию плотного метода упаковывания).
  • TightPackerPolicy по-умолчанию будет использовать плотное упаковывание, если спрайт имеет плотную сетку. Если в Packing Tag будет назначен тег “[RECT]”, то будет выполнен прямоугольный метод упаковки (т.е. присвоение вашему упаковыванию тега “[RECT]UI_Elements” приведёт к прямоугольному методу упаковывания).
  • TightPackerPolicy по-умолчанию будет использовать плотное упаковывание, если спрайт имеет плотную сетку. Если в Packing Tag будет назначен тег “[RECT]”, то будет выполнен прямоугольный метод упаковки (т.е. присвоение вашему упаковыванию тега “[RECT]UI_Elements” приведёт к прямоугольному методу упаковывания).

Настройка упаковщика спрайтов

Для большинства целей опции DefaultPackerPolicy будет достаточно, однако при необходимости вы сможете реализовать и свою собственную политику упаковки. Чтобы сделать это, вам необходимо реализовать интерфейс UnityEditor.Sprites.IPackerPolicy для класса в скрипте редактора. Данный интерфейс нуждается в следующих методах:

  • GetVersion - возвращает в виде значения версию политики вашего упаковщика. Версию политики необходимо обновить в случае, если в скрипт был изменён и эти изменения были сохранены через контроль версий.
  • OnGroupAtlases - отвечает за реализацию алгоритма упаковки. Укажите атласы в PackerJob и примените спрайты из данных TextureImporters.

DefaultPackerPolicy по-умолчанию использует прямоугольное упаковывание (см. SpritePackingMode). Это может быть полезно, если необходимо использовать текстурно-пространственные эффекты или другой меш для отображения текущего спрайта. С помощью пользовательских прав можно перезаписать эти значения и использовать вместо прямоугольного метода, плотный метод упаковывания.

  • Кнопка Repack становится доступной только тогда, когда выбрана своя политика (custom policy).
    • OnGroupAtlases will not be called unless TextureImporter metadata or the selected PackerPolicy version values change.
    • Use Repack button when working on your custom policy.
  • Sprites can be packed rotated with TightRotateEnabledSpritePackerPolicy automatically.
    • SpritePackingRotation is a reserved type for future Unity versions.

Другое

  • Atlases are cached in Project\Library\AtlasCache.
    • Deleting this folder and then launching Unity will force atlases to be repacked. Unity must be closed when doing so.
  • Кэш атласа не подгружается при запуске.
    • All textures must be checked when packing for the first time after Unity is restarted. This operation might take some time depending on the total number of textures in the project.
    • Only the required atlases are loaded.
  • Максимальный размер атласа по умолчанию равен 2048х2048.
  • При назначенном свойстве PackingTag текстуры не будут сжиматься, чтобы упаковщик спрайтов мог использовать оригинальные значения пикселей при сжатии атласов.

Политика стандартного упаковщика

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; // Good for base and two mip levels.

        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;
                    // Use Compression Quality for Grouping later only for Compressed Formats. Otherwise leave it Empty.
                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);
            }

            // First split sprites into groups based on atlas name
            var atlasGroups =
                from e in entries
                group e by e.atlasName;
            foreach (var atlasGroup in atlasGroups)
            {
                int page = 0;
                // Then split those groups into smaller groups based on texture settings
                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;
                    // Use the highest aniso level from all entries in this atlas
                    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;
        }
}

Политика плотного упаковщика

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

// TightPackerPolicy will tightly pack non-rectangle Sprites unless their packing tag contains "[RECT]".
class TightPackerPolicySample : DefaultPackerPolicySample
{
        protected override string TagPrefix { get { return "[RECT]"; } }
        protected override bool AllowTightWhenTagged { get { return false; } }
        protected override bool AllowRotationFlipping { get { return false; } }
}

Политика плотного упаковщика

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

// TightPackerPolicy will tightly pack non-rectangle Sprites unless their packing tag contains "[RECT]".
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 Editor: Edit Physics Shape
Sorting Group