Any way to speed up Sprite.Create?

I need to generate texture atlases and sprites at runtime, and I’m finding the bottleneck is Sprite.Create.

So, for instance, in one case I create 10 texture atlases, send the bits to them, and create 80 sprites (in total) from them. My timing info:

  • 0.20ms for creating the 10 textures (in total)
  • 0.08ms for LoadRawTextureData/Apply for those 10 textures (in total), 173KB total.
  • 14.3ms for SpriteCreate for 80 sprites (in total).

That last time is really surprising to me. I’m passing SpriteMeshType.FullRect to Sprite.Create, so it shouldn’t need to inspect the texture bits at all, right?

Is there any way to speed this up? What is Sprite.Create doing under the covers to take so much time? It seems like it should just be storing a rect and some other metadata in some internal structure.

Ok, to answer my own question, it looks like Unity is retrieving the texture pixels to calculate the sprite outline. Callstack for my call to Sprite.Create with SpriteMeshType.FullRect.

It doesn’t seem like it should be doing this. Maybe this is fixed in Unity 2018? (I’m using Unity 2017.4.1f1).

00efe338 0fbfd7cf UnityPlayer!prcore::RemapGeneric<TexFormatA8,TexFormatARGB8888>+0x13
00efe36c 0fbfe3fc UnityPlayer!prcore::BlitterRemapAny::Blit+0x7f
00efe3d4 0fbfdcea UnityPlayer!prcore::BlitImageRemapNoScale+0xcc
00efe494 0fcf1cdd UnityPlayer!prcore::BlitImage+0x10a
00efe4a8 0fcf1bbd UnityPlayer!BlitProphecy+0x6d
00efe4c0 0fd0f06c UnityPlayer!ImageReference::BlitImage+0x3d
00efe584 0fd0e56a UnityPlayer!Texture2D::GetPixels32+0x11c
00efe674 0fd06189 UnityPlayer!GenerateSpriteOutline+0x2da
00efe6c0 0fd074e1 UnityPlayer!Sprite::GenerateOutline+0x109
00efe6fc 0fed7d7c UnityPlayer!Sprite::Initialize+0x331
00efe748 0724e988 UnityPlayer!Sprite_CUSTOM_INTERNAL_CALL_Create+0xec

1 Like

Bump. I’d like to see this fixed too if possible. Sprite.Create causes my game to hang for a second if I call it dozens of times in a loop (Unity 2019.2.9). This happens when each player spawns their character in the pre-game lobby, I create their sprites at this point for mod support.

1 Like

Not sure if there’s a bug or not but I’d recommend pooling them early to avoid the hitch.

fwiw, I just ended up writing my own sprite implementation completely separate from Unity’s.

Mind sharing?

It’s kind of tangled up with my project and customized for it (so there’s extra stuff in here that you don’t need), but these are the two core files I’m using. The MeshRenderer/mesh used is just a default unity quad.

You need to construct FakeSprite by providing an atlas Texture, and a rect/pivot for the sprite in that atlas.

5423343–551577–FakeSprite.cs (1.87 KB)
5423343–551580–FakeSpriteRenderer.cs (3.42 KB)

1 Like

Sweet thank you!

Would it be possible to share the parameters used to call Sprite.Create and possibly give details about the Texture used to generate the Sprites from?

Sure, here’s what I’m doing:

//each sheet for this character:
        for (int i = 0; i < sheets.Length; i++) StartCoroutine(LoadTexture(sheets[i].name));
 protected IEnumerator LoadTexture(string textureName)
    {
        string filePath = "file://" + rootFolderPath + gameFolderNamePlusSkinFolderName + "/" + textureName + ".png";
        using (UnityWebRequest webRequest = UnityWebRequestTexture.GetTexture(filePath))
        {
            yield return webRequest.SendWebRequest();
            if (webRequest.isNetworkError || webRequest.isHttpError) Debug.LogError(gameObject.name + " " + webRequest.error + " " + textureName + ".png");
            else
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(webRequest);
                texture.filterMode = FilterMode.Point;
                texture.name = textureName;
                FillModDictionary(texture);
            }
        }
    }
 protected virtual void FillModDictionary(Texture2D texture)
    {
        var spritesOnThisSheet = new List<Sprite>();
        for (int i = 0; i < resourceSprites.Count; i++)
        {
            if (resourceSprites[i].texture.name == texture.name) spritesOnThisSheet.Add(resourceSprites[i]);
        }
        for (int i = 0; i < spritesOnThisSheet.Count; i++)
        {
            Sprite spriteToCopy = dictionary[texture.name + spritesOnThisSheet[i].name];
            Vector2 pivot = GetActualPivot(spriteToCopy.pivot, spriteToCopy.rect.size);
            Sprite newSprite = Sprite.Create(texture, spriteToCopy.rect, pivot, 16f);
            newSprite.name = spriteToCopy.name;
            modDictionary.Add(texture.name + newSprite.name, newSprite);
        }
    }
    protected static Vector2 GetActualPivot(Vector2 pivot, Vector2 rectSize)
    {
        return new Vector2(pivot.x / rectSize.x, pivot.y / rectSize.y);
    }

It seems that you are generating a Tight mesh for the Sprite? When the Sprite is created, this will attempt to generate a mesh based on the outline of the Sprite which involves reading the pixels of the texture. If the SpriteMeshType.FullRect is used instead, it will not do so and save time in generating the Sprite. However, this will increase the fill rate during rendering.

5 Likes

Oh thanks, didn’t know that. But I couldn’t verify a change in rect. I tried comparing fullRect vs tight but they’re printing out the same rect (the sprite slices that I feed in have plenty of transparency so I was expecting tight to produce a tighter rect):

Sprite fullSprite = Sprite.Create(texture, rect, centerPivot, 16f, 0, SpriteMeshType.FullRect);
print("full: " + fullSprite.rect);
Sprite tightSprite = Sprite.Create(texture, rect, centerPivot, 16f, 0, SpriteMeshType.Tight);
print("tight: " + tightSprite.rect);

5429427--552705--upload_2020-1-31_8-26-45.png

Am I missing something?

Oh wait duh, I realize what I’m missing. Mesh != rect

You will need to check sprite.vertices to compare the difference between SpriteMeshType.Tight and FullRect.

If you read my initial post, even when FullRect is specified, Unity reads the sprite pixels.

Sorry for not replying to this issue! This has been fixed in 2017.4.2f2, where the fallback physics shape was always generated, resulting in the pixel reading (first improvement line). If FullRect is specified without further parameters (the generatePhysicsShape parameter will be False by default but you can specify it to be False if you want), there will not be any pixel reading.

1 Like

Great news, thanks!

Man, this was a great reply. Speeded up my road sprites generation from 5.6 seconds down to 0.2 seconds. Thank you!

Could someone elaborate on this?

does this code have better performance on execution?:

Sprite fullSprite = Sprite.Create(texture, rect, centerPivot, 16f, 0, SpriteMeshType.FullRect);

even though it will have lesser performance while rendering in the scene? What if the sprite Im creating has the exact pixel bounds of the rect? Is it strictly superior in that case?

Yes, Sprite creation will be faster as using FullRect will use directly use the rect of the Sprite as the mesh for rendering, instead of reading the outline of the Sprite from its texture and tessellating that to generate the mesh for rendering for Tight.

This generally has better performance for Sprites whose rect includes a lot of empty space, as this will reduce the amount of overdraw because Sprites are considered as transparent geometry.

If the Sprite you are creating has the same outline as the rect, then the generation from using Tight will produce the same results as if you were using FullRect. If you know this beforehand, using FullRect will be better, since it will produce the same result without needing to read the outline of the Sprite and tessellate that. Since the created mesh will be the same, the rendering performance will be equal in both cases.

Do take note that the outline detection is based on the alpha values of the pixels in the texture, so there may be cases where your Sprites may appear to take up the entire rect visually, but may not be the case for outline generation.

1 Like