Sprite Sheet Slicing script troubles

Hello, I am still very new to working with Unity and have been learning as I go but here goes:

I am working on the process of taking a 3D Animated character and making a sprite sheet texture in batch of all its animations, this is working fine enough for now.

I wrote a script to set the proper settings on the texture and to slice it into the appropriate sprites, this mostly works.

The issue I have is that when a sprite sheet has “empty” space (ex. 6x6 texture of 100x100x sprites, but only 28 sprites leaving 8 “empty” spaces), these empty sprites are being generated:7570348--937381--script.png
If I simply re-slice it with the Sprite editor it respects the empty space as I would:

During my research I have found several ways to attempt slicing, but I don’t see this happening with others people using the scripts. I even went as far as replacing most of mine with snippets from others to get to some common ground. I spent a while researching for a way to validate each sprite before finalize the metadata with no luck? Is there a way to use the Sprite Editor to perform the slice operation in a script? or Perhaps a better way to approach this problem?

    static void Slicer()
    {
        string[] textureGuids = AssetDatabase.FindAssets("t:texture2D", new[] {"Assets/Textures"});
        int SliceSize = 100;
        foreach (string guid in textureGuids)
        {
            var path = AssetDatabase.GUIDToAssetPath(guid);
            TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
            if (importer == null)
            {
                Debug.Log("Null Importer: " + path);
            }
            else
            {
                if (importer.isReadable != true || importer.filterMode != FilterMode.Point || importer.spriteImportMode != SpriteImportMode.Multiple || importer.textureType != TextureImporterType.Sprite)
                {
                    importer.isReadable = true;
                    importer.filterMode = FilterMode.Point;
                    importer.spriteImportMode = SpriteImportMode.Multiple;
                    importer.textureType = TextureImporterType.Sprite;
                    AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
                    var textureSettings = new TextureImporterSettings();
                    importer.ReadTextureSettings(textureSettings);
                    textureSettings.spriteMeshType = SpriteMeshType.Tight;
                    textureSettings.spriteExtrude = 0;
                    importer.SetTextureSettings(textureSettings);
                }

                List<SpriteMetaData> smdList = new List<SpriteMetaData>();             
                Texture2D curTexture = (Texture2D)AssetDatabase.LoadAssetAtPath(path, typeof(Texture2D));
                for (int i = 0; i < curTexture.width; i += SliceSize)
                {
                    for (int j = curTexture.height; j > 0; j -= SliceSize)
                    {
                        SpriteMetaData smd = new SpriteMetaData();
                        smd.pivot = new Vector2(0.5f, 0.5f);
                        smd.alignment = 9;
                        smd.name = (curTexture.height - j) / SliceSize + ", " + i / SliceSize;
                        smd.rect = new Rect(i, j - SliceSize, SliceSize, SliceSize);
                        smdList.Add(smd);
                    }
                }

                importer.spritesheet = smdList.ToArray();
                AssetDatabase.ForceReserializeAssets(new List<string>() { path });
                AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
                AssetDatabase.SaveAssets();
            }
        }
    }

Isn’t this the answer? (from your screenshot above):

7570459--937444--Screen Shot 2021-10-13 at 3.23.39 PM.png

That would be the manual slicing in the UI. Enabling that would get the same behavior as I get in my script. I’m looking for the exact opposite behavior :smile:

I have stumbled across the below gist and its use of “InternalSpriteUtility.GenerateAutomaticSpriteRectangles” but couldn’t find any official unity documentation? Let’s see how this goes…

1 Like

Well I learned a lot about AssetPostprocessor since my last message and am now taking that path. No luck this far with GenerateAutomaticSpriteRectangles, no slicing at all to be exact, but I am going to dive into some debugging to validate each step to have more information to work with.

1 Like

Well I have some progress, I have a functional script now but I have two-ish errors I am struggling with. Here is the latest iteration.

public class TexturePostProcessor : AssetPostprocessor
{

    [MenuItem("Scripts/Reimport All Textures")]
    public static void Reimport()
    {
        AssetDatabase.ImportAsset("Assets/Textures", ImportAssetOptions.ImportRecursive);
    }


    static bool IsValidPath(string path)
    {
        return (path.Contains("Textures"));
    }

    private void OnPreprocessTexture()
    {
        if (!IsValidPath(assetImporter.assetPath)) return;
        TextureImporter importer = assetImporter as TextureImporter;
        importer.isReadable = true;
        importer.mipmapEnabled = false;
        importer.filterMode = FilterMode.Point;
        importer.spritePixelsPerUnit = 100;
        importer.alphaSource = TextureImporterAlphaSource.FromInput;
        importer.spriteImportMode = SpriteImportMode.Multiple;
        importer.textureType = TextureImporterType.Sprite;
        importer.textureCompression = TextureImporterCompression.CompressedHQ;
   }

    public void OnPostprocessTexture(Texture2D texture)
    {
        if (!IsValidPath(assetImporter.assetPath)) return;
      
        TextureImporter importer = assetImporter as TextureImporter;
        if (importer.spriteImportMode != SpriteImportMode.Multiple) return;
          
        Vector2 spriteSize = new Vector2(100, 100);
        Rect[] rects = InternalSpriteUtility.GenerateGridSpriteRectangles(texture, Vector2.zero, spriteSize, Vector2.zero);
        List<Rect> rectsList = new List<Rect>(rects);
        string filenameNoExtension = Path.GetFileNameWithoutExtension(assetPath);
        List<SpriteMetaData> metas = new List<SpriteMetaData>();
        int rectNum = 0;

        foreach (Rect rect in rectsList) {
            SpriteMetaData meta = new SpriteMetaData();
            meta.pivot = new Vector2(0.5f, 0.5f);
            meta.rect = rect;
            meta.alignment = (int)SpriteAlignment.BottomCenter;
            meta.name = filenameNoExtension + "_" + rectNum++;
            metas.Add(meta);
        }
        importer.spritesheet = metas.ToArray();

        AssetDatabase.ForceReserializeAssets(new List<string>() { assetImporter.assetPath });
    }

    public void OnPostprocessSprites(Texture2D texture, Sprite[] sprites)
    {
      
        if (!IsValidPath(assetImporter.assetPath)) return;

        if (sprites.Length == 0)
        {
            AssetDatabase.ImportAsset(assetImporter.assetPath);
            return;
        }
    }
}

Here are the errors, First is the real error:

Retrieving array element that was out of bounds
UnityEditor.AssetPostprocessingInternal:PostprocessSprites (UnityEngine.Texture2D,string,UnityEngine.Sprite[ ])

Second is shortend because its a NullRef where we are hitting the array index that is out of bounds:

NullReferenceException: Object reference not set to an instance of an object
UnityEditor.U2D.Sprites.SpriteBoneDataTransfer.Load (UnityEditor.SerializedObject importer, UnityEditor.SpriteImportMode mode, System.Int32 index) (at Library/PackageCache/com.unity.2d.sprite@1.0.0/Editor/SpriteEditorModule/TextureImporterDataProviderImplementation.cs:37)
......

Now… I understand what it means, but I am having trouble determining where the disconnect is. So far I tested this with 18 assets/spritesheets, and they all have the same issue. I even checked values with some Debug.Log to check length/count of my rects and sprite count and they all line up. I feel like I am missing something simple here.

At this point should I just create a new post since the original issue is resolved? Still learning etiquette for this forum :slight_smile:

Can you tell exactly what code is causing that? More than one of the callsites above may consider a collection of sprites internally and have an issue…

Just realized I quoted the Null Ref in a way that cuts it off. I’ll post the full exception details here shortly, but here is the cutoff text:

NullReferenceException: Object reference not set to an instance of an object
UnityEditor.U2D.Sprites.SpriteBoneDataTransfer.Load (UnityEditor.SerializedObject importer, UnityEditor.SpriteImportMode mode, System.Int32 index) (at Library/PackageCache/com.unity.2d.sprite@1.0.0/Editor/SpriteEditorModule/TextureImporterDataProviderImplementation.cs:37)

Full Exception:

NullReferenceException: Object reference not set to an instance of an object
UnityEditor.U2D.Sprites.SpriteBoneDataTransfer.Load (UnityEditor.SerializedObject importer, UnityEditor.SpriteImportMode mode, System.Int32 index) (at Library/PackageCache/com.unity.2d.sprite@1.0.0/Editor/SpriteEditorModule/TextureImporterDataProviderImplementation.cs:37)
UnityEditor.U2D.Sprites.SpriteBoneDataTransfer.GetBones (UnityEditor.GUID guid) (at Library/PackageCache/com.unity.2d.sprite@1.0.0/Editor/SpriteEditorModule/TextureImporterDataProviderImplementation.cs:27)
UnityEditor.U2D.Animation.SpritePostProcess.PostProcessBoneData (UnityEditor.U2D.Sprites.ISpriteEditorDataProvider spriteDataProvider, System.Single definitionScale, UnityEngine.Sprite[ ] sprites) (at Library/PackageCache/com.unity.2d.animation@5.0.6/Editor/SpritePostProcess.cs:107)
UnityEditor.U2D.Animation.SpritePostProcess.OnPostprocessSprites (UnityEngine.Texture2D texture, UnityEngine.Sprite[ ] sprites) (at Library/PackageCache/com.unity.2d.animation@5.0.6/Editor/SpritePostProcess.cs:29)
System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[ ] parameters, System.Globalization.CultureInfo culture) (at <695d1cc93cca45069c528c15c9fdd749>:0)
Rethrow as TargetInvocationException: Exception has been thrown by the target of an invocation.
System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[ ] parameters, System.Globalization.CultureInfo culture) (at <695d1cc93cca45069c528c15c9fdd749>:0)
System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[ ] parameters) (at <695d1cc93cca45069c528c15c9fdd749>:0)
UnityEditor.AssetPostprocessingInternal.InvokeMethodIfAvailable (System.Object target, System.String methodName, System.Object[ ] args) (at <44c3723143904fb88deebc993c7bb491>:0)
UnityEditor.AssetPostprocessingInternal.CallPostProcessMethods (System.String methodName, System.Object[ ] args) (at <44c3723143904fb88deebc993c7bb491>:0)
UnityEditor.AssetPostprocessingInternal.PostprocessSprites (UnityEngine.Texture2D tex, System.String pathName, UnityEngine.Sprite[ ] sprites) (at <44c3723143904fb88deebc993c7bb491>:0)

This is Unity 2020.3.15f1

Just to add, I put some Debug.Log entries at different points and it happens after all my code runs. I am just concerned I am doing something silly and messing up the index count of the sprites, but from what I can tell this should not be the case

That and what the callstack reveals makes me think this is definitely something inside Unity throwing the exception.

It is still possible it is because of something incorrect in your importer, but Unity might also have a bug.

I would pluck this code out into a new project (with the necessary packages) with just enough sprite textures to show the problem and a) make sure it still happens there, then file a bug. When you file a bug Unity zips up your entire project, making it kind of annoying to file bugs from your actual game if it is big.

Will Do! Luckily this is all happening in a 2D URP (not from template) so the project itself is already quite small as my import script and the spritesheets are all that is in there so that would be easy to do. I might dupe the project and test with newer unity versions to see if it is consistent first.

Any luck with this? This was the only thread I have found yet that actually resulted in an automatic import of the sub assets (other code without the AssetDatabase.ForceReserializeAssets(new List<string>() { assetImporter.assetPath }); and

if (sprites.Length == 0)
        {
            AssetDatabase.ImportAsset(assetImporter.assetPath);
            return;
        }

sections would result in a sprite with multiple sprites listed in the sprite editor, but without the arrow menu with individual sprites inside it. Going into the sprite editor and either reslicing, or even changing any property and applying, would result in generating the sprites - AND the out of range and null reference errors. Something weird is definitely going on. :confused:

Not sure why OnPostprocessSprites receives a Sprite[ ] argument of length 0 though. And it won’t reprocess them without both the above pieces of code.

Just realized my errors are not exactly the same. My current SpriteProcessor code looks like this:

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;

public class SpriteProcessor : AssetPostprocessor
{
    void OnPreprocessTexture()
    {
        // This could be for example "Art/Character/Sprites/" or "Art/Enemy/Sprites".
        if (assetPath.Contains("/Sprites/"))
        {
            Debug.Log("Processing texture " + assetImporter.assetPath);
            TextureImporter textureImporter = (TextureImporter)assetImporter;
            textureImporter.textureType = TextureImporterType.Sprite;
            textureImporter.spriteImportMode = SpriteImportMode.Multiple;
            textureImporter.spritePixelsPerUnit = 32f;
            textureImporter.wrapMode = TextureWrapMode.Clamp;
            textureImporter.filterMode = FilterMode.Point;
            // We can enable/edit this if needed. 2048 is default anyway.
            // Smaller sizes may improve performance if we have an issue here.
            // textureImporter.maxTextureSize = 2048;
            textureImporter.textureCompression = TextureImporterCompression.Uncompressed;

        }
    }


    void OnPostprocessTexture(Texture2D texture)
    {
        Debug.Log("Texture2D: (" + texture.width + "x" + texture.height + ")");

        Vector2 spriteSize = new Vector2(64, 64);
        Rect[] spriteRects = InternalSpriteUtility.GenerateGridSpriteRectangles(texture, Vector2.zero, spriteSize, Vector2.zero, true);

        Debug.Log(spriteRects);
        Debug.Log(spriteRects.Length);

        List<SpriteMetaData> spriteMetas = new List<SpriteMetaData>();
        string fileName = Path.GetFileNameWithoutExtension(assetPath);

        TextureImporter textureImporter = (TextureImporter)assetImporter;

        for (int i = 0; i < spriteRects.Length; i++)
        {
            SpriteMetaData meta = new SpriteMetaData();
            meta.rect = spriteRects[i];
            meta.name = fileName + "_" + i;
            spriteMetas.Add(meta);
        }
        textureImporter.spritesheet = spriteMetas.ToArray();

        AssetDatabase.ForceReserializeAssets(new List<string>() { assetImporter.assetPath });
        AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);

    }

    void OnPostprocessSprites(Texture2D texture, Sprite[] sprites)
    {
        Debug.Log("Found Sprites: " + sprites.Length);
    }
}

Dragging and dropping a sprite into a Sprites folder with this code in effect produces the following two errors twice, each at the end of triggering the OnPostprocessTexture block (which happens twice):

Retrieving array element that was out of bounds
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)

and

NullReferenceException: Object reference not set to an instance of an object
UnityEditor.U2D.Sprites.SpriteBoneDataTransfer.Load (UnityEditor.SerializedObject importer, UnityEditor.SpriteImportMode mode, System.Int32 index) (at Library/PackageCache/com.unity.2d.sprite@1.0.0/Editor/SpriteEditorModule/TextureImporterDataProviderImplementation.cs:37)
UnityEditor.U2D.Sprites.SpriteBoneDataTransfer.GetBones (UnityEditor.GUID guid) (at Library/PackageCache/com.unity.2d.sprite@1.0.0/Editor/SpriteEditorModule/TextureImporterDataProviderImplementation.cs:27)
UnityEditor.U2D.Animation.SpritePostProcess.PostProcessBoneData (UnityEditor.U2D.Sprites.ISpriteEditorDataProvider spriteDataProvider, System.Single definitionScale, UnityEngine.Sprite[] sprites) (at Library/PackageCache/com.unity.2d.animation@5.0.6/Editor/SpritePostProcess.cs:107)
UnityEditor.U2D.Animation.SpritePostProcess.OnPostprocessSprites (UnityEngine.Texture2D texture, UnityEngine.Sprite[] sprites) (at Library/PackageCache/com.unity.2d.animation@5.0.6/Editor/SpritePostProcess.cs:29)
System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at <695d1cc93cca45069c528c15c9fdd749>:0)
Rethrow as TargetInvocationException: Exception has been thrown by the target of an invocation.
System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at <695d1cc93cca45069c528c15c9fdd749>:0)
System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) (at <695d1cc93cca45069c528c15c9fdd749>:0)
UnityEditor.AssetPostprocessingInternal.InvokeMethodIfAvailable (System.Object target, System.String methodName, System.Object[] args) (at <ef3b6bf002d8435a97b4e938f6c49b02>:0)
UnityEditor.AssetPostprocessingInternal.CallPostProcessMethods (System.String methodName, System.Object[] args) (at <ef3b6bf002d8435a97b4e938f6c49b02>:0)
UnityEditor.AssetPostprocessingInternal.PostprocessSprites (UnityEngine.Texture2D tex, System.String pathName, UnityEngine.Sprite[] sprites) (at <ef3b6bf002d8435a97b4e938f6c49b02>:0)
UnityEditorInternal.InternalEditorUtility:ProjectWindowDrag(HierarchyProperty, Boolean)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)

I have the same two Errors. Has anyone found a solution?