Sprite Packer: Why are my sprites in different "groups"?

When I create a “Landscape” packing tag, the sprite packer usually puts them all in one texture…except, sometimes, it doesn’t. Even when all of the attributes of the images are identical, sometimes it throws one of them into a different atlas. I would like these things to all be put into a single atlas so I can minimize my draw calls and memory usage, but it seems determined not to let this happen.

What am I missing? What settings are used to determine what goes on each “group”?

I’ve included the two groups (the one with a bunch of sprites is “group 0”, the one with only warning lines is “group 1”). As you can see there is more than enough empty space in group 0, and you can see that the import settings for the warning lines are identical to the settings for one of the sprites that IS included in group 0. (Except for the pivot, and if that’s a difference that causes it to be in a different atlas, I’m going to file that one under “Yeah, that’s a bug.”)

EDIT: I’ve figured it out: The warningline were being imported as “RGB” rather than “ARGB”. Is there any way to force Unity to import something as ARGB even if there is no relevant alpha channel (specifically for situations like this)? I opened it up in Photoshop and made a tiny corner of the texture transparent to get Unity to import it that way, but sheesh, that’s a gross hack.

1645431--102284--$Screen Shot 2014-05-29 at 11.31.56 AM.png
1645431--102285--$Screen Shot 2014-05-29 at 11.32.01 AM.png


1 Like

That’s how default packing policies behave. They’re very strict on texture format.
You could write a custom policy which pretends there’s alpha in all textures. Just take code from Unity - Manual: Sprite Packer and change “entry.settings.format = ins.desiredFormat” to convert from RGB to RGBA and possibly other variants you need.

1 Like

A simpler solution than a custom packer is to change your non-A RGB textures from Sprite to Advanced, then in the dropdown at the bottom of the import settings change to an RGBA format. This worked for me and got things packing really nicely.

3 Likes

this does not really work because the default format changes when you change platforms
so if i change my solid sprites to ARGB32 to fit into the main sprite sheet on the web platform it works fine, until i change to android platform and the default changes to RGBA32 and my solid sprites are left out again

i got it working like this but:

  • might there be some negative implications for changing a sprite from solid to fake transparent? this is big solid background image so idk
  • might there be negative implications for changing the texture format of the sprite sheet to rgba32 on all platforms? usually web is argb32?

is there some way i can set it to match the default or current sprite sheet format? web argb32, android rgba32, etc

like maybe i could use this to get the platform format?
http://docs.unity3d.com/ScriptReference/TextureImporter.GetPlatformTextureSettings.html
but it takes a platform string? how can i determine the platform and access or generate this string? or maybe a better way

thanks

True @rakkarage - I’m working on android and iOS and they stay consistent on that. If I test on standalone, I get split sheets again. It’s infuriating.

1 Like

I made a custom Packer Policy (based on the default packer policy on the Unity page) that forces the opaque textures into the transparent atlases.
I only updated it for PC/WebGL/iPhone/Android - so you may need to add others (check which formats are splitting into separate atlases in the Sprite Packer window).

This is the part I added after the texture settings are read:

            // Custom - Forcing opaque textures into alpha atlas - consolidate atlases
            if (desiredFormat == TextureFormat.DXT1)
            {
                desiredFormat = TextureFormat.DXT5;
            }
            else if (desiredFormat == TextureFormat.ETC_RGB4)
            {
                desiredFormat = TextureFormat.ETC2_RGBA8;
            }
            else if (desiredFormat == TextureFormat.PVRTC_RGB4)
            {
                desiredFormat = TextureFormat.PVRTC_RGBA4;
            }

Here is the full script for convenience:

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


public class AlphaDefaultPackerPolicy : 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);

            // Custom - Forcing opaque textures into alpha atlas - consolidate atlases
            if (desiredFormat == TextureFormat.DXT1)
            {
                desiredFormat = TextureFormat.DXT5;
            }
            else if (desiredFormat == TextureFormat.ETC_RGB4)
            {
                desiredFormat = TextureFormat.ETC2_RGBA8;
            }
            else if (desiredFormat == TextureFormat.PVRTC_RGB4)
            {
                desiredFormat = TextureFormat.PVRTC_RGBA4;
            }
            // End Custom

            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;
    }
}
2 Likes