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!