2D sprite rendering: Efficient way to switch texture atlases

Hi all,

I have created a 2D sprite rendering engine in C++ and OpenGL. Each “Drawable” entity has a sprite attached to it and can be drawn at a different position, pivot point (origin), angle, scale and depth. A Sprite refers to a different texture page and texture coords in it, based on each sub-image.

I have avoided instancing since I have read that it’s not very efficient for drawing quads. Same for point sprites/geometry shaders.

So far the project has been a success! In order to maximize batching, each Drawable has a unique ID with the following setup in a 64-bit long long int:

First X bits represent the depth (the sign bit is left untouched)
Next Y bits represent the texture page ID
The last Z bits represent the drawable ID.

Therefore Drawables are sorted based on their Unique ID in Drawable Chunks and data from these chunks go to their respective VBO, EBO & VAO setup. Chunks and their respective buffer data are updated only when a drawable changes position, angle, depth etc. In this way, by using glMultiDrawElements(), the quads are being drawn in their proper order with the maximum amount of batching possible for the same texture page. Even if I acquire a subset of Drawable entities (say, by getting them form a Quadtree) and I sort them based on their unique ID, their order is the same as in their respective chunks since the sorting criterion is the same.

My question is the following:
I use glBindTexture to set up the proper texture page whenever a batch breaks due to the current drawable group belonging to a different texture page than the one being currently in use. Since Drawables can have different depths and can belong to different texture pages, despite their sorting/batching, a lot of texture swaps still occur.

Example: Drawing 25K sprites at random depths ranging from 0 to 1000, from 2 different texture pages, can result in 2K batches/texture swaps. Obviously, the larger the depth range, the more the batches: A depth range from 0 to 8 yields only ~60 different batches.

Is there a more efficient way to assign a texture page when a batch breaks? I tried binding a Texture to a different Texture Unit and then simply switching the active texture unit with glActiveTexture without calling glBindTexture, but this doesn’t seem to work. The fragment shader currently samples just from one texture unit (the default). Is there any other configuration I can look up (like texture arrays or setting up a lot of active textures and passing which sampler to use as a uniform)?

I would appreciate any info on this!

EDIT: In a realistic scenario there wouldn’t be so many different depths. Even in complex 2D games most drawable entities align to some grid and therefore many groups of entities share the same depth. My question is more on a technical level rather than a practical one. I am still learning about OpenGL and would like some pointers on what to look up next!

  • If all atlases can all reside in the GPU, you can use bindless textures to avoid texture binding.
  • If all atlases have the same size (or most of them), you can use Array textures.
  • You can also play with the texture units.

After reading your question, bindless texture is the first thing that came to my mind as well:

This lets you potentially access huge number of textures within a single batch, without any need to bind them before the batch is launched. You can even select which ones to sample dynamically in the shader logic.

Note this caveat though (listed on the wiki page):

So on NVIDIA GPUs/drivers, bindless textures are great! Pass in or compute the texture ID however you want, lookup the corresponding texture handle, cast it to a sampler, and do the lookup. On other vendors’ GPUs/drivers, watch out for the above caveat.

Texture arrays are a fallback to bindless texture if you can’t/don’t want to use bindless textures, though they come with some disadvantages compared to bindless texture (e.g. within a single tex array, same res, same internal format, etc.). Texture atlases are a 2nd level fallback from that, and come with even more disadvantages.

glActiveTexture() doesn’t determine what the shader does. It determines which texunit subsequent glBindTexture() operations operate on.

In the shader (with bound textures), you’d switch between sampling texture unit A and texture unit B by changing your texture sampling over to using a different sampler uniform that also passed into the shader. But before you get too excited, there are major limitations on indexing into “arrays of samplers”. In GLSL 3.3, you can only index sampler arrays with constants. In GLSL 4.0, they extended that to uniforms (Dynamically Uniform Integral Expressions; see EXAMPLES). This limits your options…

Thank you for your replies. I am currently using OpenGL 3.3. Bindless textures indeed look promising but the aforementioned caveat does look scary.

Is it possible to combine texture atlases (with arbitrary sprite sizes/positions within) and texture arrays? All atlases have the same dimensions/image format, no mipmaps. I read the wiki page and I haven’t found why I couldn’t do this.

Yes. You can use mipmaps with texture arrays, although they aren’t that useful if the levels are atlases. Also, you can’t use a linear magnification filter with atlases as that will result in “bleeding” (interpolating between texels from different sprites) at the edges. Using 2D array textures instead of atlases (i.e. one layer per sprite) avoids these issues, but is inefficient if your sprites have differing sizes; the other issue is that that array textures aren’t required to support more than 256 layers, which may not be enough if you have many small sprites.

You can have arrays of samplers, but the index must be constant in GLSL 3.x or dynamically uniform in GLSL 4.x, so that isn’t much help.

But I can! All one has to do is add a border around each sprite in the atlas and, within that border, repeat the sprite’s edge pixels. I tested this in Unity some time ago and it works perfectly.

About the sampler arrays, this part gets me a bit confused. I suspect you refer to using texture units. Could you please provide a small example on how a sampler array would be used?

Edit: In case it wasn’t clear, my previous question was “can I use texture arrays consisting of texture atlases”, not “can I use texture arrays and texture atlases in the same program”. Let me know if this changes your reply.

You can use array textures where each level is an atlas. There aren’t any significant differences between atlases in array textures and atlases in 2D textures. Using array textures instead of atlases has some advantages over atlases (mipmaps, clamp-to-edge), but also some disadvantages (limited number of layers, and most useful if all sprites are the same size).

Sampler arrays are e.g. uniform sampler2D textures[4]. You need one texture unit for each element in the array, and the array can only be indexed with a constant (3.x) or dynamically uniform (4.x) expression; you can’t index the array with e.g. an attribute. So it isn’t much use for reducing the number of batches.

@GClements and others, thanks for pointing me in the right direction. I will look into a texture atlas/array combination first. You are being very helpful.