Unity's new UI system released in version 4.6 is great. A quality UI system was long overdue for Unity, and they've done a very nice job. However, there are some pitfalls - one big problem is runtime sprite generation, and how incredibly slow it can be! This is especially true when using ETC texture compression for mobile deployment. However, there are ways around the issue!
First, let's explore the problem. I will assume you're familiar with sprite atlases and their purpose of reducing draw calls.
Loading individual sprites into the editor and letting Unity pack our sprite atlases is a breeze with the new UI system. However, what about times when we want to load our sprites from a remote location, such as a web server? One of our projects currently in development utilises a web based CMS for much of the project art assets, allowing rapid updates without requiring an application update. This means we need to generate a very large number of sprites from downloaded textures, and Unity presents us with two possible options:
1. Pack the sprite atlases in the app.
This was ruled out pretty quickly as sprite packing at runtime is slow - especially on mobile devices. This makes sense, as there's a lot going on when it comes to sprite atlas packing.
2. Pack the sprite atlases on the server.
Sprite packing algorithms can be simple to implement, especially when the image sizes are known in advance. (As in our case.)
So, with our sprite atlases being packed on the server, compressed using ETC1 and delivered to the client as compressed PVR files, all that's left is to create the sprite from the atlas and apply it to the UI element.
But there's one more issue here, and it's a big one. Generating sprites using compressed atlases at runtime is incredibly slow. Let's do some benchmarks with a simple test case.
Our test sprite atlas. Sprites created by Franco Giachetti - LudicArts.com.
Our test project - a 3x3 grid of images, with the 9 sprites being created at runtime from within a C# script using our pre-packed atlas above.
Using System.Diagnostics.Stopwatch, I time the creation of the sprites (created using Sprite.Create) and output the result in milliseconds to a text element. The results, as tested on a Samsung Galaxy S5 running Android 5.0:
1024 x 1024 PNG atlas loaded as RGBA 16 bit.
3 milliseconds to generate 9 sprites - pretty good. 5.3mb of texture memory used - not good.
1024 x 1024 PVR atlas loaded as ETC 4 bit.
312 milliseconds to generate 9 sprites - not good. 0.5mb of texture memory used - good.
So, it seems we have the option of speed or low memory overhead, but we can't have both? Fortunately, there's technically no reason we need to create sprites at runtime, as there's a much faster alternative available using the RawImage component.
A different approach...
As all we really want to do is limit what region of the sprite atlas is being drawn on the UI element, why not just adjust the UV Rect of the element? This option isn't possible using the standard Image component, however the RawImage component makes the UV Rect accessible in the inspector and via scripts using the .uvRect parameter. There's just some simple arithmetic required to normalise our sprite rect - divide all X positions and widths by the atlas width, and all Y positions and heights by the atlas height. That's all!
With our new implementation, our new "sprite" generation time is 1 millisecond! Even faster than the uncompressed atlas, but with the low memory overhead of the compressed atlas. Best of all, as changes to the UV Rect don't affect the material, we're still just using a single draw call. It's always nice to have your cake and eat it.