Combining Sprite Renderers Into Texture Atlases At Runtime

The Need for Texture Atlases

Texture atlases can significantly improve graphics rendering performance in video games and simulations by reducing the number of draw calls issued to the GPU. Each individual texture used by sprite renderers requires its own draw call to switch and bind that texture before issuing render commands. This texture switching introduces CPU and GPU overhead resulting in slower frame rates, especially for scenes with many small sprites.

By packing sprites into larger atlas textures, many sprites can reuse the same atlas texture instead of binding their own texture every frame. This allows the scene geometry to be batched into fewer draw calls, issuing render commands for many sprites at once without costly texture binds in between. The reduction in draw calls and texture switching provides large performance gains.

Texture atlases also avoid the need to repeatedly load and unload individual texture assets over the course of a game. This prevents hitches and frame drops from texture streaming during demanding scenes. Atlases also simplify memory use across platforms and GPU architectures compared to managing many individual texture resources.

Reducing Draw Calls for Better Performance

The primary benefit of texture atlases comes from reducing the number of draw calls, which reduces CPU workload and avoids GPU pipeline flushes. Every sprite renderer that uses a unique texture must bind that texture before issuing its render commands. Binding a texture requires submitting commands to the graphics driver, overhead that quickly adds up.

By packing sprites into atlas textures, multiple sprite renderers can share an atlas texture binding before batching all their geometry into a single draw call. This allows hundreds of sprites to be rendered with just a few texture binds and draw calls instead of one per sprite. This reduces graphics driver communication and 3D API overhead significantly.

Avoiding Texture Swapping for Smooth Rendering

Reducing texture binds through an atlas also provides rendering smoothness benefits. Constantly switching out textures mid-frame can cause uneven frame pacing and stutter even at high frame rates. The GPU graphics pipeline must flush when switching textures, which interrupts the rendering sequence.

Packed into an atlas, many sprites can render without texture switches. The uneven pipeline flushing and texture streaming is avoided, allowing for a steady stream of draw calls using the atlas texture. This provides smoother, more consistent rendering leading to a better gameplay experience.

Generating Texture Atlases at Runtime

While texture atlases are commonly created offline during game development, generating atlases at runtime provides unique advantages. With dynamic runtime packing, sprite textures can be packed to fit available space precisely for optimal use of VRAM. Level-specific sprites can share atlases matching their active visual complexity. Runtime atlases also enable dynamic loading and unloading of texture assets not known until gameplay begins.

Dynamically Packing Sprites into Atlas Textures

Creating texture atlases at runtime requires dynamically packing sprite textures into larger atlas textures on-demand. As new sprites are activated, the texture regions they require must be allocated out of available space in existing atlases, or new atlases generated to hold additional sprites.

Dynamic texture packing is an optimization challenge similar to the bin packing problem in computing. Algorithms must track available space in atlas textures and expand with new textures as needed based on texture size limits and renderer format support. Regions must also be tracked and reused when sprites are deactivated to support dynamic loading.

Handling Asset Loading and Unloading

With runtime atlases, sprite texture assets must be individually loadable on demand to pack into atlases. Game asset systems built offline often bundle together atlas textures assuming fixed content. Supporting dynamic runtime behavior requires loading individual sprite textures separate from any atlas resources.

Run-time atlases also permit older sprite textures to be unloaded over time while keeping active sprites packed into newer atlases. This further reduces overall VRAM use beyond compile-time atlases, while supporting unlimited sprite collections through streaming and repacking.

Updating Batch Render Commands

As sprite textures are packed and unpacked from the generated atlas textures, the rendering commands for those sprites must be updated. Sprites packed together into an atlas texture can batch their draw calls together, while sprites removed revert back to their individual assets and draw calls.

To support this dynamic rendering, some form of indirection lookup is needed to map from sprite render components to their current texture atlas assignment used for issuing draw commands. Resolving these mappings each frame ensures rendering always targets the correct atlas texture or assets based on the latest packing state.

Managing Sprite Renderers

At runtime, rendering behavior for sprite components must be managed to use the generated atlas textures properly. As sprites are assigned to different atlases over time from the packing process, their rendering setup must also update.

Assigning Textures from Atlas

The first key aspect is assigning the correct runtime atlas texture to use for batches of sprites. A texture atlas manager looks up the assigned atlas based on sprite properties and sets this on the renderer state before issuing a batch draw call.

Care must be taken to handle both packed sprites pointing to an atlas texture, versus unpacked standalone sprites using their original source assets. The render logic chooses the appropriate path when configuring each sprite batch.

Applying Correct Texture Coordinates

Sprite renderers must also use the proper UV texture coordinates into their assigned locations inside the atlas texture. Rather than UV coordinates going across the original sprite asset, they now reference a tightly fit sub-region inside the atlas texture to sample.

The atlas mapping tracks these UV rects for render components. Updating the UV rect drives the correct region sampling from an atlas, crucial for proper animation displays. Multiple sprites share space within an atlas texture so unique UV rects isolate content.

Handling Sprite Sorting and Layers

When rendering sprite batches, special handling is often needed to sort them front-to-back or apply blending behaviors. Transparent sprite effects require specific draw order for proper alpha blending results.

With runtime atlases, these behaviours must continue to work across batches of different packed sprites. This can be achieved through secondary sort and proxy buffers rather than sorting packed vertex data itself. This allows rendering algorithms to apply across an entire atlas batch uniformly.

Optimizing Atlases Over Time

While initial atlas packing provides immediate improvements, runtime atlases enable further optimization over time. As gameplay occurs, statistics can be analyzed to reconfigure atlases for ideal VRAM use. Atlases can also be unloaded entirely when no longer needed.

Monitoring Render Statistics

By tracking rendering statistics, the runtime atlas system can estimate performance costs. This assists the dynamic packer in making optimal use of limited atlas space to maximize performance.

Statistics like texture bind counts, draw calls per frame, or renderer workload timing inform decisions around consolidating atlases, expanding with more textures, or unpacking lesser used sprites. This feedback loop ensures optimal packing in response to actual runtime rendering data.

Periodically Re-packing Atlases

Over the course of gameplay, levels and visual complexity change significantly in real-world games. The active working set of sprites and thus atlas content can shift into suboptimal patterns even with initial dynamic packing.

By revisiting atlas packing with updated information periodically, unused space can be consolidated and layout improved to current behavior. Defragmenting based on runtime statistics helps keep packing efficient long-term across hours of gameplay.

Unloading Old Atlas Textures

Lastly, atlas textures holding sprites no longer needed can be unloaded outright to recover VRAM no longer actively used. Old atlas textures not referenced by any sprite renderers for some time can be flagged as inactive then fully unloaded.

This process allows practically unlimited sprite counts to stream through the system over time, while limiting peak memory use to currently visible subsets. Only a few actively referenced atlases stay resident rather than accumulating indefinitely.

Example Code for Runtime Texture Atlas

Implementing runtime texture atlasing behaviors requires several key systems working together. While often engineered uniquely per game engine, similar principles apply across rendering architectures.

Pseudocode Walkthrough for Key Systems

At a high level, primary behaviors needed are:

  • DynamicTexturePacker
    • InsertSpriteTexture(spriteTexture)
    • GetAtlasForSprite(sprite)
  • AtlasManager
    • LoadAtlasTexture()
    • UnloadAtlasTexture()
    • MapSpriteToAtlas(sprite, atlas)
  • SpriteRenderer
    • reference to assigned AtlasTexture
    • AtlasUVRect
    • DrawPass for sorting/blending
  • Renderer
    • IssueDraw(atlasTexture)
    • SetUVRect(rect)
    • SubmitSprite(rendererState)
    • IssueAtlasDrawBatch()

Pseudocode omits detail around memory management, concurrency, and other systems for simplicity. But this broadly covers updating sprites, assigning to atlases, and rendering using shared atlas batches.

C# Code Snippets for Implementation

In C#, key classes could be defined with relationships:

// Dynamic packer
public sealed class TexturePacker {

  public AtlasTexture PackSprites(List textures) {   
    // Pack logic
  } 

}

// Atlas manager
public class AtlasManager {
  
  Dictionary loadedAtlases;   

  public void LoadAtlas(AtlasTexture atlas) {
    // Loading logic
  }

  public void UnloadAtlas(AtlasTexture atlas) {
   // Unloading logic
  }

  public void MapSpriteToAtlas(SpriteRenderer renderer, AtlasTexture atlas) {
   // Assignment logic
  }

}

// Renderer structures 
public class SpriteRenderer {

  public AtlasTexture atlas;
  public Rect uvRect;
  
  public void SubmitToBatcher() {
    // Submit to sort/batch
  }

  public void Draw() {
    // Issue draw based on atlas 
  }

}

public class Renderer {

  public void DrawAtlasBatch(AtlasTexture atlas) {
    // Bind atlas & issue batch    
  }

}

This demonstrates one approach to connecting the core logic between packer, atlas manager, renderer, and draw batching that dynamically assigns and renders sprites using generated atlas textures at runtime.

Leave a Reply

Your email address will not be published. Required fields are marked *