Updating Partial Tilemap Textures For Smooth Scrolling

The Tilemap Scrolling Challenge

Tilemaps are a popular technique in 2D games for constructing expansive worlds and levels by combining small reusable texture tiles. However, smoothly scrolling large tilemaps poses performance challenges, as the graphics hardware must regularly update textures for all visible tiles.

During scrolling, most tiles shift by only a few pixels each frame. Yet, traditionally the entire textures for hundreds of tiles are reloaded from scratch each frame. This causes redundant pixel processing and slowdowns.

The key insight is that we only need to update the subset of pixels that change on shifted tiles each frame. By only updating partial tile textures each frame, we can optimize performance and achieve smooth 60 FPS scrolling.

Understanding Partial Texture Updates

To understand how partial updates work, first consider how a tile’s texture is stored in VRAM. The texture comprises a 2D grid of color values for each pixel. As the tile scrolls during gameplay, the texture grid shifts within the tile’s sprite boundaries.

Now observe how pixel colors shift between frames during scrolling. Most pixels move to an adjacent grid cell, inheriting the color of their neighbor. Only pixels along the edges are uncovered or wrapped to the far side.

So instead of updating all texture pixels each frame, we can detect edge pixels that need updates, and upload small partial update textures. This saves redundant pixel processing and VRAM transfers.

Detecting Dirty Regions

To implement partial updates, we first detect “dirty regions” – edges where the tile texture needs updating. As tiles shift by sub-tile pixel deltas each frame, dirty regions appear on opposite edges.

Dirty region geometry depends on the sub-pixel shift amount. Efficient dirty rect detection formulas must handle wrapping and symmetry cases for all possible shift amounts.

Generating Partial Textures

Once dirty regions are detected, we generate partial update textures covering just these areas. Care is needed when wrapping pixels from one edge to the opposite side.

We copy pixel colors from the matching opposite edge, and fill any remaining holes using neighboring tiles or procedural colors. This creates a small valid texture update for just the dirty regions.

Implementing Partial Updates in Unity

Let’s now walk through implementing reusable ScrollableTilemap components in Unity supporting partial updates.

Detecting Visible Tiles

Our first task is detecting which tiles are visible each frame as the camera moves. Unity provides geometry culling APIs for this based on the camera frustum and view volume.

We utilize culling to get arrays of the visible tile grid coordinates. This reduces our update workload from all map tiles to just tiles onscreen.

Tiling and Grid Lookup

Underneath, our tilemap resides in a 2D grid structure. To support fast indexing, we wrap this grid around horizontally and vertically into a continuous tiling space.

This enables simple lookup from visual scroll space into tile grid coordinates. We encapsulate tiling math in the TileAddress structure providing grid array access.

Visibility Processing

Our ScrollableTilemap class handles visibility processing each frame in Update(). First we check for any camera movement and record new visible tile addresses.

We add these to our TileVisibilitySet – a cache tracking which tiles need texture updates. Later stages act only on visible tiles to maximize efficiency.

Uploading Updated Textures

With our tile visibility set updated, we now generate and upload partial update textures.

In the UpdateTextures() method, we iterate over visible tiles. For each we detect dirty regions, generate a partial update texture, and upload it to the GPU.

Generating Updates

The TileTextureGenerator class handles taking a base tile texture, shifting it by the scroll deltas, detecting dirty regions, and emitting an updated partial texture.

Special processing handles wrapping pixels, replicating edges, and filling missing areas. Careful region clipping provides pixel-perfect updates.

Uploading and Caching

As update textures are generated, we immediately upload them to GPU VRAM via a TextureUpdateStream class. This handles pooling texture memory and updating active sprite renderer materials.

Uploaded textures are cached for only the necessary few frames based on scroll speeds. This prevents redundant regeneration and uploads each frame.

Example Code

Here is some example C# code from our ScrollableTilemap implementation:

public class ScrollableTilemap : MonoBehaviour {

  TileVisibilitySet visibleTiles;	

  void Update() {
    if (CameraMoved()) {
      visibleTiles.Update(CameraViewVolume); 
    }
  }

  void LateUpdate() {	
    UpdateTextures();
  }	
	
  void UpdateTextures() {
    foreach (TileAddress tile in visibleTiles) {
	
      // 1) Detect dirty regions			
      Rect[] dirtyAreas = CalculateDirtyRegions(tile);
	
      // 2) Generate partial update texture
      Texture2D texture = GenerateTextureUpdate(tile, dirtyAreas);

      // 3) Upload update	
      TextureUpdateStream.Update(tile.SpriteRenderer, texture);

    }
  }
}

public struct TileAddress {
  public int x;
  public int y;	
	
  public SpriteRenderer SpriteRenderer {
    get { 
      return tilemap.GetSpriteAt(x, y); 
    }
  }
}

Performance Considerations

There are many optimization angles around updating tilemap textures during scrolling:

  • Minimize visible tiles processed using culling
  • Only update textures for tiles entering or leaving the view
  • Tune generated texture size and update frequency
  • Pool similar texture updates in VRAM
  • Prioritize updates by screen position

Finding the right balance is key – updating too rarely causes visual glitches, while updating too often hurts frame rates.

Limiting Updates

An initial approach is to only update tiles textures when they first become visible each time. However this causes mid-scroll flickering as tiles suddenly switch textures.

A better strategy is updating textures 1-2 frames after visibility changes. This avoids redundant updates, while preventing visual glitches.

Variable Update Frequencies

We can shrink update workload by reducing update frequency for tiles far from the view center. Tiles near screen edges update less often without visibility changes.

We encode update priority in the TileVisibilitySet based on last update frame. This way we concentrate texture updates where they are most visible.

Achieving 60 FPS During Scrolling

By applying partial tile updates we can optimize GPU texture processing to reach silky smooth 60 FPS scrolling.

In practice we see 4-8x performance gains from partial updates compared to naive full texture reloads.

Less new pixel data needs uploading and processing each frame. Plus VRAM access patterns are more efficient for small contiguous texture regions.

Faster effective texture upload and sample bandwidth enables meeting the 16ms render budget for 60 FPS. The result is smoothly scrolling expansive 2D worlds.

Further Optimization Techniques

Partial tile updates are a great starting point for optimizing scroll performance. Further ideas to explore include:

  • Predictively preload textures for tiles about to become visible
  • Compress textures in VRAM and decompress on the GPU
  • Analyze frame time spikes to target bottleneck areas
  • Render lower LOD textures for far tiles to save fillrate

With some analysis and profiling, you can push scrolling performance even further!

Leave a Reply

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