Search Unity

Sprite Editor Automatic Slicing by Script

Discussion in 'Editor & General Support' started by BehemothPro, Apr 22, 2015.

  1. BehemothPro

    BehemothPro

    Joined:
    Feb 14, 2015
    Posts:
    19
    I'm trying to find a way to automatically slice a sprite texture into multiple sprites through an editor script instead of manually having to click the slice button for each of my textures. Is there a function I can call for that?
     
    floriancourgey likes this.
  2. TomasJ

    TomasJ

    Joined:
    Sep 26, 2010
    Posts:
    256
  3. BehemothPro

    BehemothPro

    Joined:
    Feb 14, 2015
    Posts:
    19
    Thanks, but I was hoping for a way to set the SpriteMetaData through Unity's method of slicing or is that not accessible?
     
  4. aer0ace

    aer0ace

    Joined:
    May 11, 2012
    Posts:
    1,513
    I wanted to add to this, since I sort of had the same problem, yet there were several broken solutions that I found online.
    Below is one way of doing it, with the assumption that the spritesheet consists of evenly-cut frames, say exported from aseprite or something. My frames are each 64x64.

    The process goes like this:
    1. OnPreprocessTexture() -> Set up the TextureImporter. At this point, you can't set up the SpriteMetadata, which specifies the individual Sprites in the Spritesheet, because the Texture hasn't been processed yet.
    2. OnPostProcessTexture() -> Now that the texture has been processed, I have the dimensions of the texture, which I can use to slice up evenly, and determine the rows and columns in the sprite sheet, and store in the SpriteMetadata in the TextureImporter
    3. OnPostprocessSprites() -> This should result in the sliced sprites as the second param.

    Code (CSharp):
    1. public class SpriteProcessor : AssetPostprocessor
    2. {
    3.     void OnPreprocessTexture ()
    4.     {
    5.         TextureImporter textureImporter = (TextureImporter)assetImporter;
    6.         textureImporter.textureType = TextureImporterType.Sprite;
    7.         textureImporter.spriteImportMode = SpriteImportMode.Multiple;
    8.         textureImporter.mipmapEnabled = false;
    9.         textureImporter.filterMode = FilterMode.Point;
    10.  
    11.     }
    12.  
    13.     public void OnPostprocessTexture (Texture2D texture)
    14.     {
    15.         Debug.Log("Texture2D: (" + texture.width + "x" + texture.height + ")");
    16.  
    17.      
    18.  
    19.         int spriteSize = 64;
    20.         int colCount = texture.width / spriteSize;
    21.         int rowCount = texture.height / spriteSize;
    22.  
    23.         List<SpriteMetaData> metas = new List<SpriteMetaData>();
    24.  
    25.         for (int r = 0; r < rowCount; ++r)
    26.         {
    27.             for (int c = 0; c < colCount; ++c)
    28.             {
    29.                 SpriteMetaData meta = new SpriteMetaData();
    30.                 meta.rect = new Rect(c * spriteSize, r * spriteSize, spriteSize, spriteSize);
    31.                 meta.name = c + "-" + r;
    32.                 metas.Add(meta);
    33.             }
    34.         }
    35.  
    36.         TextureImporter textureImporter = (TextureImporter)assetImporter;
    37.         textureImporter.spritesheet = metas.ToArray();
    38.     }
    39.  
    40.     public void OnPostprocessSprites(Texture2D texture, Sprite[] sprites)
    41.     {
    42.         Debug.Log("Sprites: " + sprites.Length);
    43.     }
    44. }
     
    Wichenstaden likes this.
  5. sarahnorthway

    sarahnorthway

    Joined:
    Jul 16, 2015
    Posts:
    78
    You can now (Unity 2017.3) access InternalSpriteUtility.GenerateAutomaticSpriteRectangles to automatically slice up a texture into sprites during OnPostprocessTexture. Code:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections.Generic;
    4. using UnityEditorInternal;
    5. using System.IO;
    6.  
    7. // https://forum.unity.com/threads/sprite-editor-automatic-slicing-by-script.320776/
    8. // http://www.sarpersoher.com/a-custom-asset-importer-for-unity/
    9. public class SpritePostprocessor : AssetPostprocessor {
    10.  
    11.     /// <summary>
    12.     /// Default all textures to 2D sprites, pivot at bottom, mip-mapped, uncompressed.
    13.     /// </summary>
    14.     private void OnPreprocessTexture() {
    15.         Debug.Log("OnPreprocessTexture overwriting defaults");
    16.  
    17.         TextureImporter importer = assetImporter as TextureImporter;
    18.         importer.textureType = TextureImporterType.Sprite;
    19.  
    20.         importer.spriteImportMode = SpriteImportMode.Multiple;
    21.  
    22.         importer.mipmapEnabled = true;
    23.         importer.filterMode = FilterMode.Trilinear;
    24.         importer.textureCompression = TextureImporterCompression.Uncompressed;
    25.     }
    26.  
    27.     public void OnPostprocessTexture(Texture2D texture) {
    28.         TextureImporter importer = assetImporter as TextureImporter;
    29.         if (importer.spriteImportMode != SpriteImportMode.Multiple) {
    30.             return;
    31.         }
    32.  
    33.         Debug.Log("OnPostprocessTexture generating sprites");
    34.  
    35.         int minimumSpriteSize = 16;
    36.         int extrudeSize = 0;
    37.         Rect[] rects = InternalSpriteUtility.GenerateAutomaticSpriteRectangles(texture, minimumSpriteSize, extrudeSize);
    38.         List<Rect> rectsList = new List<Rect>(rects);
    39.         rectsList = SortRects(rectsList, texture.width);
    40.  
    41.         string filenameNoExtension = Path.GetFileNameWithoutExtension(assetPath);
    42.         List<SpriteMetaData> metas = new List<SpriteMetaData>();
    43.         int rectNum = 0;
    44.  
    45.         foreach (Rect rect in rectsList) {
    46.             SpriteMetaData meta = new SpriteMetaData();
    47.             meta.rect = rect;
    48.             meta.name = filenameNoExtension + "_" + rectNum++;
    49.             metas.Add(meta);
    50.         }
    51.  
    52.         importer.spritesheet = metas.ToArray();
    53.     }
    54.  
    55.     public void OnPostprocessSprites(Texture2D texture, Sprite[] sprites) {
    56.         Debug.Log("OnPostprocessSprites sprites: " + sprites.Length);
    57.     }
    58.  
    59.     private List<Rect> SortRects(List<Rect> rects, float textureWidth) {
    60.         List<Rect> list = new List<Rect>();
    61.         while (rects.Count > 0) {
    62.             Rect rect = rects[rects.Count - 1];
    63.             Rect sweepRect = new Rect(0f, rect.yMin, textureWidth, rect.height);
    64.             List<Rect> list2 = this.RectSweep(rects, sweepRect);
    65.             if (list2.Count <= 0) {
    66.                 list.AddRange(rects);
    67.                 break;
    68.             }
    69.             list.AddRange(list2);
    70.         }
    71.         return list;
    72.     }
    73.  
    74.     private List<Rect> RectSweep(List<Rect> rects, Rect sweepRect) {
    75.         List<Rect> result;
    76.         if (rects == null || rects.Count == 0) {
    77.             result = new List<Rect>();
    78.         } else {
    79.             List<Rect> list = new List<Rect>();
    80.             foreach (Rect current in rects) {
    81.                 if (current.Overlaps(sweepRect)) {
    82.                     list.Add(current);
    83.                 }
    84.             }
    85.             foreach (Rect current2 in list) {
    86.                 rects.Remove(current2);
    87.             }
    88.             list.Sort((Rect a, Rect b) => a.x.CompareTo(b.x));
    89.             result = list;
    90.         }
    91.         return result;
    92.     }
    93. }
    94.  
     
    lofell, shshwdr, FranekKimono and 3 others like this.
  6. ChiuanWei

    ChiuanWei

    Joined:
    Jan 29, 2012
    Posts:
    131
    not good. and how to call the sprite editor slice function
     
  7. MrCabbages

    MrCabbages

    Joined:
    Oct 25, 2016
    Posts:
    2
    @sarahnorthway This doesn't seem to do anything for me. It's running properly on import, but always generating 0 sprites.

    Quick Edit: I've fixed it but I have no idea why. Right before you call GenerateAutomaticSpriteRectangles, I added a "Debug.Log(texture)" to make sure it wasn't null... and now it works. If I take out the Debug.Log, it doesn't work. I'm on 2018b13

    Quick Edit 2: I took it out to test it and added it back in and now it doesn't work again, even with the debug.log in place. Should've just left it alone.
     
    Last edited: Apr 24, 2018
  8. Speedrun-Labs

    Speedrun-Labs

    Joined:
    Aug 1, 2014
    Posts:
    16
    After several hours of dickin' around with the script I found the answer!

    Add: AssetDatabase.Refresh(); After this line: importer.spritesheet = metas.ToArray();


    Note: when you import textures and the script runs there will be a console message saying:
    "A default asset was created for 'Assets/blah.png' because the asset importer crashed on it last time."

    Anyone know what this means or how to fix it? (The script still works fine, just don't like errors ;) )
     
    Last edited: May 11, 2018
    FranekKimono likes this.
  9. Speedrun-Labs

    Speedrun-Labs

    Joined:
    Aug 1, 2014
    Posts:
    16
  10. Alex_May

    Alex_May

    Joined:
    Dec 29, 2013
    Posts:
    198
    Note for travellers: InternalSpriteUtility.GenerateAutomaticSpriteRectangles currently does not work properly for NPOT textures. Of course, your textures shouldn't be NPOT anyway, but there it is.
     
    sevencrane and chenshajie like this.
  11. me_gone_mad1

    me_gone_mad1

    Joined:
    Feb 2, 2018
    Posts:
    9
    # late to the party but
    i remixed sarahnorthway's and aer0ace's code,
    the premise it auto slices to get a count of sprites in the sheet, then picks the largest sprites center and finds all sprites that are in the same row/column to get the row/column count then uses those to get a grid slice.

    Code (CSharp):
    1.     public void OnPostprocessTexture(Texture2D texture)
    2.     {
    3.         if (assetPath.ToLower().IndexOf("/spritesheets/") == -1)
    4.             return;
    5.         int minimumSpriteSize = 16;
    6.         int extrudeSize = 0;
    7.         Rect[] rects = InternalSpriteUtility.GenerateAutomaticSpriteRectangles(texture, minimumSpriteSize, extrudeSize);
    8.         var A_Sprite = rects.OrderBy(r => r.width * r.height).First().center;
    9.         int colCount = rects.Where(r => r.Contains(new Vector2(r.center.x, A_Sprite.y))).Count();
    10.         int rowCount = rects.Where(r => r.Contains(new Vector2(A_Sprite.x, r.center.y))).Count();
    11.         Vector2Int spriteSize = new Vector2Int(texture.width / colCount, texture.height / rowCount);
    12.  
    13.         List<SpriteMetaData> metas = new List<SpriteMetaData>();
    14.  
    15.         for (int r = 0; r < rowCount; ++r)
    16.         {
    17.             for (int c = 0; c < colCount; ++c)
    18.             {
    19.                 SpriteMetaData meta = new SpriteMetaData();
    20.                 meta.rect = new Rect(c * spriteSize.x, r * spriteSize.y, spriteSize.x, spriteSize.y);
    21.                 meta.name = string.Format("#{3} {0} ({1},{2})", Path.GetFileNameWithoutExtension(assetImporter.assetPath), c, r,r*colCount+c);
    22.                 metas.Add(meta);
    23.             }
    24.         }
    25.  
    26.         TextureImporter textureImporter = (TextureImporter)assetImporter;
    27.         textureImporter.spritesheet = metas.ToArray();
    28.         AssetDatabase.Refresh();
    29.     }
    if any one can see any problems let me know :)
     
    IEdge likes this.
  12. IEdge

    IEdge

    Joined:
    Mar 25, 2017
    Posts:
    51
    Unfortunately it does not work with all the sprite sheets
     
  13. KingKong320

    KingKong320

    Joined:
    Mar 2, 2018
    Posts:
    21
    when i download the image and assign it to gameobject's sprite Renderer,And after that if i quit game,where does that downloaded image goes?
    Is it in my device or have to download again on next time opening game?
     
  14. losingisfun

    losingisfun

    Joined:
    May 26, 2016
    Posts:
    36
    In Unity 2020+ none of the above methods will work. The Slice feature is a bit more involved now and uses some previously undocumented parts of Unity (https://docs.unity3d.com/Manual/Sprite-data-provider-api.html).

    This is what I ended up writing to get this working (Hope this helps someone else in the future):
    Code (CSharp):
    1. public static Sprite[] SliceTexture(Texture2D texture, int sliceWidth, int sliceHeight)
    2.     {
    3.         List<Sprite> slicedSprites = new List<Sprite>();
    4.  
    5.         string assetPath = AssetDatabase.GetAssetPath(texture);
    6.         TextureImporter textureImporter = AssetImporter.GetAtPath(assetPath) as TextureImporter;
    7.  
    8.         if (textureImporter == null)
    9.         {
    10.             Debug.LogError("Failed to get TextureImporter for texture");
    11.         }
    12.         else
    13.         {
    14.             // FIRST STEP:
    15.             // Setup the texture asset to have the correct import settings
    16.             // Eg, Ensure that the texture is set to 'multiple' mode
    17.             textureImporter.textureType = TextureImporterType.Sprite;
    18.             textureImporter.spriteImportMode = SpriteImportMode.Multiple;
    19.             textureImporter.spritePixelsPerUnit = 16;
    20.             textureImporter.mipmapEnabled = false; // Mipmaps are unnecessary for sprites
    21.             textureImporter.textureCompression = TextureImporterCompression.Uncompressed;
    22.             textureImporter.maxTextureSize = 8192; // Im setting this much larger incase we have very large spritesheets
    23.  
    24.             // Reimport the texture with updated settings
    25.             AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
    26.             //-----------------------------------
    27.          
    28.             // SECOND STEP:
    29.             // Slice the texture and create SpriteRects that represent each sliced sprite
    30.             // Note: TextureImporter.spritesheet no longer works, it seems to get overridden
    31.             var factory = new SpriteDataProviderFactories();
    32.             factory.Init();
    33.             ISpriteEditorDataProvider dataProvider = factory.GetSpriteEditorDataProviderFromObject(texture);
    34.             dataProvider.InitSpriteEditorDataProvider();
    35.             dataProvider.SetSpriteRects(GenerateSpriteRectData(texture.width, texture.height, sliceWidth, sliceHeight));
    36.             dataProvider.Apply();
    37.  
    38.             var assetImporter = dataProvider.targetObject as AssetImporter;
    39.             assetImporter.SaveAndReimport();
    40.             //------------------------------------
    41.  
    42.             // THIRD STEP:
    43.             // I use this to load all of the sliced sprites into a sprite array that I use in a scriptable object
    44.             Object[] assets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
    45.             foreach (Object asset in assets)
    46.             {
    47.                 if (asset is Sprite sprite && sprite.name != texture.name)
    48.                 {
    49.                     slicedSprites.Add(sprite);
    50.                 }
    51.             }
    52.         }
    53.  
    54.         return slicedSprites.ToArray();
    55.     }
    56.  
    57.     private static SpriteRect[] GenerateSpriteRectData(int textureWidth, int textureHeight, int sliceWidth, int sliceHeight)
    58.     {
    59.         List<SpriteRect> spriteRects = new List<SpriteRect>();
    60.      
    61.         for (int y = textureHeight; y > 0; y -= sliceHeight)
    62.         {
    63.             for (int x = 0; x < textureWidth; x += sliceWidth)
    64.             {
    65.                 SpriteRect spriteRect = new SpriteRect();
    66.                 spriteRect.rect = new Rect(x, y - sliceHeight, sliceWidth, sliceHeight);
    67.                 spriteRect.pivot = new Vector2(0.5f, 0f);
    68.                 spriteRect.name = $"Slice_{x / sliceWidth}_{y / sliceHeight}";
    69.                 spriteRect.alignment = SpriteAlignment.BottomCenter;
    70.                 spriteRect.border = new Vector4(0,0,0,0);
    71.  
    72.                 spriteRects.Add(spriteRect);
    73.             }
    74.         }
    75.  
    76.         return spriteRects.ToArray();
    77.     }
    This method takes a texture and the width/height to grid slice the texture. It then loads all the cut sprites and returns them as an array (You might not need this part, but I used it in my case to assign it to a scriptable object)