Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Sprite Atlas + Sprite Subassets?

Discussion in '2D' started by GamerXP, Nov 20, 2019.

  1. GamerXP

    GamerXP

    Joined:
    Mar 22, 2014
    Posts:
    74
    How do you pack sprites created with Sprite.Create in sprite atlases?
    I have a Tile script that generates a texture and sprites based on other sprite.

    I want to pack this into atlas, but it seems to unable to find those assets. If I put them directly in "Objects for Packing" list of a sprite atlas - it either does nothing or crash Unity.
    Is this even possible in some way or I have to generate normal image asset somewhere for this to work?
     
  2. Bunderant

    Bunderant

    Joined:
    May 29, 2013
    Posts:
    21
    Have you found any solutions/workarounds here? I've been struggling with this issue, too. The SpriteAtlas seems to be doing some weird caching where it only scans my ScriptableObject for subassets if I choose SpriteAtlas import settings that haven't been used yet.

    For example, when I first create the atlas, I'll add my parent folder to the packing list via the SpriteAtlas inspector, hit "pack preview" and everything will show up correctly both in the preview and when playing the game. But, if I were to move another asset with nested Sprite assets to my packed folder, hitting "pack preview" again doesn't update the preview and won't inject the added Sprite references in Play Mode.

    However, with the additional assets still in my packed folder, if I change the SpriteAtlas import settings in some way (like switching from "point" to "bilinear" texture filtering, all the additional Sprite assets show up in the preview correctly, as well as in Play Mode. BUT! If I change the importer settings back to the original state, the added Sprites are missing both from the preview, and once again not bound to the atlas in Play Mode. I'm very confused.
     
  3. Bunderant

    Bunderant

    Joined:
    May 29, 2013
    Posts:
    21
    Whoa. @GamerXP apparently the issues I just described in my last post are fixed in 2019.3.3. I saw there was some SpriteAtlas work done in the release notes, so I gave it a shot. Hopefully it fixes your issues, as well.
     
  4. GamerXP

    GamerXP

    Joined:
    Mar 22, 2014
    Posts:
    74
    I'll try it later, but I doubt they will fix that. I already send this a bug report long ago, and support said "This is intended (too niche problem), we won't fix that", just like 95% of my other niche problem reports.
     
  5. GamerXP

    GamerXP

    Joined:
    Mar 22, 2014
    Posts:
    74
    Checked 2019.3.3. It still does not pack sprites in atlases it seems, but now it doesn't crash when you directly reference sprites inside scriptable object for atlases, and it actually packs them - not sure if they work fine after than though.
     
  6. Bunderant

    Bunderant

    Joined:
    May 29, 2013
    Posts:
    21
    Man, yeah, this is really bizarre. I tried to recreate a minimal representation of what I've done, but the sprites aren't packing in what I've whipped up. The only difference between this minimal case I've made and what has worked is that the working version uses a custom ScriptedImporter, where this version uses AssetPostprocessor to create the sub objects. Here's the minimal non-working case I just attempted. Maybe I've just done something stupid here, but I can't see why these wouldn't pack.


    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections.Generic;
    4.  
    5. /// <summary>
    6. /// Initializes newly created SpriteContainerAssets with random sprites and an atlas.
    7. /// </summary>
    8. sealed class SpriteContainerPostprocessor : AssetPostprocessor
    9. {
    10.     private static List<string> _createdAssets = new List<string>();
    11.  
    12.     /// <summary>
    13.     /// Built-in callback for AssetPostprocessor subclasses.
    14.     /// </summary>
    15.     void OnPreprocessAsset()
    16.     {
    17.         // Cache asset paths that were just created for use in postprocessing.
    18.         if (assetImporter.importSettingsMissing)
    19.         {
    20.             _createdAssets.Add(assetPath);
    21.         }
    22.     }
    23.  
    24.     /// <summary>
    25.     /// Built-in callback for AssetPostprocessor subclasses.
    26.     /// </summary>
    27.     static void OnPostprocessAllAssets(
    28.         string[] importedAssets,
    29.         string[] deletedAssets,
    30.         string[] movedAssets,
    31.         string[] movedFromAssetPaths)
    32.     {
    33.         bool bDidCreateSpriteContainers = false;
    34.  
    35.         foreach (string path in importedAssets)
    36.         {
    37.             bDidCreateSpriteContainers |= TryInitializeSpriteContainer(path);
    38.         }
    39.  
    40.         if (bDidCreateSpriteContainers)
    41.         {
    42.             AssetDatabase.SaveAssets();
    43.         }
    44.  
    45.         _createdAssets.Clear();
    46.     }
    47.  
    48.     // Populates newly created SpriteContainerAssets with a Sprite objects and a backing Texture2D.
    49.     private static bool TryInitializeSpriteContainer(string path)
    50.     {
    51.         var asset = (SpriteContainerAsset)AssetDatabase.LoadAssetAtPath(path, typeof(SpriteContainerAsset));
    52.  
    53.         if (asset != null && _createdAssets.Remove(path))
    54.         {
    55.             GenerateTestSprites(16, 16, 1, 8, out Texture2D atlas, out Sprite[] sprites);
    56.  
    57.             AssetDatabase.AddObjectToAsset(atlas, path);
    58.             foreach (Sprite sprite in sprites)
    59.             {
    60.                 AssetDatabase.AddObjectToAsset(sprite, path);
    61.             }
    62.  
    63.             return true;
    64.         }
    65.  
    66.         return false;
    67.     }
    68.  
    69.     /// <summary>
    70.     /// Creates a horizontal texture strip with randomly colored sprites.
    71.     /// </summary>
    72.     private static void GenerateTestSprites(
    73.         int width,
    74.         int height,
    75.         int padding,
    76.         int count,
    77.         out Texture2D atlas,
    78.         out Sprite[] sprites)
    79.     {
    80.         int atlasWidth = width * count + (padding * (count - 1));
    81.         int atlasHeight = height;
    82.  
    83.         Texture2D texture = new Texture2D(
    84.             atlasWidth,
    85.             atlasHeight,
    86.             TextureFormat.RGBA32,
    87.             false);
    88.  
    89.         texture.name = "Atlas";
    90.         texture.filterMode = FilterMode.Point;
    91.  
    92.         sprites = new Sprite[count];
    93.  
    94.         for (int spriteIdx = 0; spriteIdx < count; spriteIdx++)
    95.         {
    96.             int x = (width + padding) * spriteIdx;
    97.             int y = 0;
    98.  
    99.             Sprite sprite = Sprite.Create(
    100.                 texture,
    101.                 new Rect(x, y, width, height),
    102.                 new Vector2(0.5f, 0.5f),
    103.                 height);
    104.  
    105.             sprite.name = spriteIdx.ToString("D4");
    106.  
    107.             FillSpriteWithColor(
    108.                 sprite,
    109.                 new Color(
    110.                     Random.Range(0f, 1f),
    111.                     Random.Range(0f, 1f),
    112.                     Random.Range(0f, 1f)
    113.                 ));
    114.  
    115.             sprites[spriteIdx] = sprite;
    116.         }
    117.  
    118.         void FillSpriteWithColor(Sprite sprite, Color color)
    119.         {
    120.             for (int x = (int)sprite.rect.xMin; x < (int)sprite.rect.xMax; x++)
    121.             {
    122.                 for (int y = (int)sprite.rect.yMin; y < (int)sprite.rect.yMax; y++)
    123.                 {
    124.                     sprite.texture.SetPixel(x, y, color);
    125.                 }
    126.             }
    127.         }
    128.  
    129.         texture.Apply();
    130.         atlas = texture;
    131.     }
    132. }
     
  7. Bunderant

    Bunderant

    Joined:
    May 29, 2013
    Posts:
    21
    For the sake of completeness, here's the SpriteContainerAsset class:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu(menuName = "SpriteContainer")]
    4. public sealed class SpriteContainerAsset : ScriptableObject
    5. { }
     
  8. Bunderant

    Bunderant

    Joined:
    May 29, 2013
    Posts:
    21
    @GamerXP Okay, finally got a minimal working example, if you're interested. Using the ScriptedImporter is the trick, and you can save your ScriptableObject instances via AssetDatabase.CreateAsset with whatever extension you want.

    First, here's the ScriptableObject. I had to have my own "Create" menu item so I could use a custom extension:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. public sealed class SpriteContainerAsset : ScriptableObject
    5. {
    6.     public const string EXT = "spritecontainer";
    7.  
    8.     [MenuItem("Assets/Create/SpriteContainer")]
    9.     public static void Create()
    10.     {
    11.         EditorUtility.FocusProjectWindow();
    12.  
    13.         var asset = CreateInstance<SpriteContainerAsset>();
    14.  
    15.         ProjectWindowUtil.CreateAsset(
    16.             asset,
    17.             GetUniqueAssetPathInCurrentDirectory("New Sprite Container", EXT)
    18.         );
    19.     }
    20.  
    21.     /// <summary>
    22.     /// Gets a unique file path in the currently selected project directory.
    23.     /// Modified from https://gist.github.com/allanolivei/9260107#gistcomment-3096277
    24.     /// </summary>
    25.     /// <param name="name">Asset name.</param>
    26.     /// <param name="extension">Extension.</param>
    27.     /// <returns></returns>
    28.     public static string GetUniqueAssetPathInCurrentDirectory(string name, string extension)
    29.     {
    30.         string directoryPath = "Assets";
    31.  
    32.         foreach (var obj in Selection.GetFiltered<Object>(SelectionMode.Assets))
    33.         {
    34.             var path = AssetDatabase.GetAssetPath(obj);
    35.             if (string.IsNullOrEmpty(path))
    36.             {
    37.                 continue;
    38.             }
    39.  
    40.             if (System.IO.Directory.Exists(path))
    41.             {
    42.                 directoryPath = path;
    43.                 break;
    44.             }
    45.             else if (System.IO.File.Exists(path))
    46.             {
    47.                 directoryPath = System.IO.Path.GetDirectoryName(path);
    48.                 break;
    49.             }
    50.         }
    51.  
    52.         return AssetDatabase.GenerateUniqueAssetPath($"{directoryPath}/{name}.{extension}");
    53.     }
    54.  
    55.     /// <summary>
    56.     /// Creates a horizontal texture strip with randomly colored sprites.
    57.     /// </summary>
    58.     public static void GenerateTestSprites(
    59.         int width,
    60.         int height,
    61.         int padding,
    62.         int count,
    63.         out Texture2D atlas,
    64.         out Sprite[] sprites)
    65.     {
    66.         // Same as from method in my previous post
    67.     }
    68. }
    Next, the custom importer, which uses the extension constant defined in my ScriptableObject class:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor.Experimental.AssetImporters;
    3.  
    4. [ScriptedImporter(1, new string[] { SpriteContainerAsset.EXT }, AllowCaching = true)]
    5. public class SpriteContainerImporter : ScriptedImporter
    6. {
    7.     public override void OnImportAsset(AssetImportContext ctx)
    8.     {
    9.         SpriteContainerAsset.GenerateTestSprites(16, 16, 1, 8, out Texture2D atlas, out Sprite[] sprites);
    10.  
    11.         ctx.AddObjectToAsset(atlas.name, atlas);
    12.         foreach (Sprite sprite in sprites)
    13.         {
    14.             ctx.AddObjectToAsset(sprite.name, sprite);
    15.         }
    16.     }
    17. }
    The sprites generated here work as expected with SpriteAtlas. Just a side note, this example is creating new random sprites on every import, which you probably wouldn't want to do. And my method to get the current file path isn't that robust, gotta warn ya. Might freak out if you try to create an asset in a Packages subdirectory or something.
     
    Last edited: Feb 28, 2020
  9. GamerXP

    GamerXP

    Joined:
    Mar 22, 2014
    Posts:
    74
    Yes, I already knew it worked with ScriptableImporter just fine. That was exactly what I reported to the support - it's inconsistent behaviour. But that went nowhere in the end.

    Here is the last reply I got. It was on the 4th of January. I properly sent more examples after that, but got no reply yet.

     
  10. Bunderant

    Bunderant

    Joined:
    May 29, 2013
    Posts:
    21
    Oh, gotcha. Didn't realize you'd already found a workaround. Anyway, glad I worked through it and it's all out in the open. Hopefully some other poor confused souls such as myself will stumble across this if they find themselves similarly afflicted.
     
    Last edited: Feb 28, 2020
  11. GamerXP

    GamerXP

    Joined:
    Mar 22, 2014
    Posts:
    74
    Here is an offical reply from Unity's support concerning this:

    Unfortunately, I have been informed that the Sprite Atlas does not support assets created with the ScriptableObject but it supports sprite assets created with ScriptedImporter.​

    It seems they got no plan on changing how it works.. like usual with such niche problems. So, yes, you have to either use custom file formats with scriptable importers for that, or store your textures in separate assets.
     
  12. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,319
    GamerXP likes this.