Texture Atlas Overhaul – BB – Update 7

close upTexture Atlas Overhaul – BB – Update 7close up

Posted: December 19, 2024

Last Updated: December 20, 2024

Texture Atlas Overhaul – BB – Update 7

Timeframe: December 13, 2024 – December 19, 2024

!!! Jump to Visual Demos !!!

Welcome to another update. I bring you tidings of improved rendering performance, and just a week since the last one!

HIGHLIGHTS:

I redid my approach to texture atlases.

Static Cache to Instance Cache:

My initial texture atlas created an instance out of a static cache of textures changed only in SpriteAssetManager. I initially only intended to create a texture atlas for custom sprites imported from the user’s project folder, but of course I refused to let myself do it the stupid way any longer. How would I be able to create one texture atlas for pixel art and one texture atlas for bilinear texture filtering by interacting with a single static cache of imported textures? The answer was to not do that. Thus, I shifted away from my static caching approach (quick, dirty, and easy) to a more applicable instanced approach. This choice will help in the future with different types of imported sprites, but it also helped with rendering speed in general, as you’ll see.

First, I made a new TextureAtlas class that keeps an instanced cache after initialization instead of a static cache. Looking back, I don’t know why I didn’t do that in the first place, but whatever. This allows me to make multiple TextureAtlas instances that each contain their own cache of textures which, again, will help in the future.

However, this isn’t the only improvement this allowed. Now, I would be able to create more TextureAtlases for other scenarios.

Rendering Speed Improvements:

One problem that I am constantly running into is the issue of having too many RenderArchetypes tracked by NSprites. Baking sprites that use different textures and materials will lead to more RenderArchetypes tracked by NSprites, and more RenderArchetypes leads to a longer processing time in the SpriteRenderingSystem.To fix this problem, I turned to my newly-revised TextureAtlas.

I then developed a system that would find the textures associated with different NSprites RenderArchetypes and compact them all into a single RenderArchetype unified under one texture atlas. This way, the SpriteRenderingSystem would not have to process as many archetypes for rendering. In other words, when you have a bunch of sprites in your subscene that come from different textures/assets, instead of all of them getting a unique RenderArchetype, they are pushed into the same RenderArchetype, just with different UV coordinates.

To test the system, I added 20 temporary objects to a subscene and gave them each a sprite from different imported assets. After doing so, 28 RenderArchetypes were shown in the NSprites inspector. Using my new timing tools, I measured the update time for SpriteRenderingSystem, averaging about 4.8ms for the system update. With a budget of about 16.67ms per frame (for 60fps), this takes over 25% of our entire allowed frame time!

Then, I enabled the texture atlas system. When run, the 28 archetypes are condensed down into a svelte 6 archetypes, taking only 1ms (~6%) of frame time. Even more promising is that when opening a project, a similar thing occurs with some separated sprites in the builder subscenes, and more archetypes are condensed and replaced with custom sprites from a texture atlas. Both processes result in a total of 4 archetypes and an average SpriteRenderingSystem update time of ~0.8ms. How nice!

Hooray for the TextureAtlas!

COOL NEW UTILITIES:

Texture Atlases:

  • TextureAtlasMap: Removed.
  • ITextureAtlas: An interface outlining various atlas actions (GetUV, GetScale, GetTexture, Build, etc.).
  • TextureAtlas: A basic texture atlas. Cache named textures in a TextureAtlas instance and call Build. It will take those named, cached textures and pack them into one texture. Get UV and other information for a single sprite by querying for its name in the TextureAtlas.
  • AdaptiveTextureAtlas: The same as TextureAtlas, except with an adaptive size. The basic TextureAtlas shrinks textures to fit into the final texture, but the AdaptiveTextureAtlas retries the texture packing with a larger output texture each time it has to shrink an input texture. The resulting atlas should have no shrunken constituent textures.
  • Artifact Fix: While redoing the texture atlas, I managed to fix a bug that caused an artifact box to show around sprites from the texture atlas. The problem was that I was using a compressed texture format and not applying padding to the outside of the atlas.

Stopwatches:

  • StopwatchRecorder: A new class to record a sliding window of times recorded by the C# Stopwatch class.
  • StopwatchTracker: A static class that contains a mapping between stopwatch names (string) and StopwatchRecorders. This can be used to retrieve relevant existing StopwatchRecorders in any other spot of the code for time profiling.
  • StopwatchRecorderContext: A class like StopwatchRecorder, but used with a ‘using’ clause to automatically, start, stop, and log stopwatch values for a block of code.

Sliding Windows:

Got tired of redoing similar code in so many places for tracking sliding windows of values, so I made some utils.

  • SlidingWindow<T>: Keeps track of n most recent values of type T where n is called the window size.
  • ComparableSlidingWindow<T>: Same as SlidingWindow except we limit T to where T is an IComparable. This way we can track the maximum and minimum values in the current window.

Debugging:

  • Added a LinePlotter class from an old project to show line plots.
  • Added a UILineRenderer that provides an interface for plotting lines on a UI element. Adapted code used here.

Video Demos

Fixed texture atlas mapping artifacting. We compare a native prefab sprite (red circle) and a custom-imported sprite (soda) before and after the artifact fix.

20 new sprites chilling in a testing subscene resulting in 28 total render archetypes.

Profiling SpriteRenderingSystem update times before opening a project. Without the texture atlas fix (left), we get ~4ms and with the fix (right), ~0.5ms.

Profiling SpriteRenderingSystem update times after opening a project. In both cases, we reduce the number of archetypes a bit anyways, so each performs better. Without the texture atlas fix, we get ~3.5ms (left) and with the fix ~0.033ms (right).