2
\$\begingroup\$

I'm making a 2D game engine, and I need a way to batch all textures into one so I only make one glBindTexture call throughout a scene. I am trying to use 2D texture arrays, but they only support textures of the same size.

Do I have to conform to this, or is there a workaround?

\$\endgroup\$

3 Answers 3

5
\$\begingroup\$

What user1118321 says is true -- all the textures need to be the same size.

But that doesn't mean you need to USE all the space. For a small set of standalone textures, it's not a big deal to waste some space.

You can simply store the U/V coordinates of where a smaller texture ends and use that. When creating the array, just set a max size and make sure everything is <= that.

Here's some code from my project that I use to do this -- it uses some data structures not presented here, so you'll have to modify it a bit, but the idea should help. This works in 3.3 + a common extension or two (I forget which ones) or 4.3 (4.4?) without any (I think).

// this takes a vector containing the raw image data from the files (I use lodepng) as well as some sizing data also returned from lodepng
// max width/height are the actual size of the textures to be created
GLuint Graphics::make_texture_array(std::vector<ArrayTextureData> & 
textures, size_t max_width, size_t max_height, bool repeatable) {
    GLuint texture_name;
glGenTextures(1, &texture_name);
glBindTexture(GL_TEXTURE_2D_ARRAY, texture_name);
std::vector<u_int32_t> clear_data(max_width * max_height, 0);

//  printf("Creating texture array with %d textures and  max texture width %d and max texture height %d\n", (int)textures.size(), max_width, max_height);
    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 3, GL_RGBA8, max_width, max_height, textures.size());

    for (int i = 0; i < textures.size(); i++) {
        auto & texture = textures[i];
//      printf("Loading texture into array at position %d actual texture w: %d actual texture h: %d with %d bytes\n", i, texture.width, texture.height, (int)texture.data.size());
        assert(texture.width <= max_width);
        assert(texture.height <= max_height);
        // set the whole texture to transparent (so min/mag filters don't find bad data off the edge of the actual image data)
        glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, max_width, max_height, 1, GL_RGBA, GL_UNSIGNED_BYTE, &clear_data[0]);
        glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, texture.width, texture.height, 1, GL_RGBA, GL_UNSIGNED_BYTE, &texture.data[0]);
        CHECK_GL_ERROR;
        texture.min_u = 0;
        texture.min_v = 0;
        texture.layer = i;
        texture.max_u = texture.width / static_cast<float>(max_width);
        texture.max_v = texture.height / static_cast<float>(max_height);
        texture.texture_id = texture_name;
    }
    if (repeatable) {
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
    } else {
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    }

    // makes enlarged textures run faster but look awful
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
    CHECK_GL_ERROR;

    //Unbind texture
    glBindTexture( GL_TEXTURE_2D_ARRAY, NULL);


    return texture_name;
}
\$\endgroup\$
8
  • 2
    \$\begingroup\$ Note that this breaks wrapping or clamping texture filters and may give you surprising samples around the virtual edges of your texture. \$\endgroup\$ May 10, 2016 at 7:12
  • \$\begingroup\$ My textures tend to have transparent borders, so I guess it's just not an issue for me, but good callout. \$\endgroup\$
    – xaxxon
    May 10, 2016 at 9:15
  • \$\begingroup\$ I added some code that I use to do this. It's probably not perfect and you'll have to adapt it to your own data structures, but I think it's relatively clear what it does \$\endgroup\$
    – xaxxon
    May 10, 2016 at 19:52
  • \$\begingroup\$ @xaxxon Thank you so much. Would it be a good idea for me to use more (about 3 or 4) array textures of different max sizes to reduce unfilled data? They would be sized icon, small, medium, large, etc. \$\endgroup\$ May 10, 2016 at 20:44
  • \$\begingroup\$ @fridgemagnet I mean, really it depends how much stuff you're loading.. but when even crappy integrated stuff has at least 512MB, it takes a lot of empty space before it starts to matter. Also, there's no reason you couldn't programatically load up a few really small ones onto a single texture and have the min_u/min_v not be 0 for them - a an array of texture atlases. \$\endgroup\$
    – xaxxon
    May 10, 2016 at 20:53
1
\$\begingroup\$

I'm not sure what you mean by "so I only make one glBindTexture call throughout a scene." What advantage does that get you?

2D texture arrays require the textures to be the same size. I don't know of any way around that.

One other option you have is to use a texture atlas. You can make a single texture that contains all of your assets and simply pass the appropriate texture coordinates for each texture use. I found this tutorial. It uses OpenGL ES 2, but it should give you the general idea.

\$\endgroup\$
1
  • \$\begingroup\$ Using many glBindTextures per frame will screw with my performance, so I need to bind one big texture array for each scene of the game. \$\endgroup\$ May 10, 2016 at 19:16
0
\$\begingroup\$

If you're willing to restrict yourself to drivers which expose Bindless Textures you can make all the textures you want resident, put their handles in an uniform or structured buffer as sampler2D values and use those transparently in your shader.

If not, you could have a series of texture arrays, one for each size and in your shader code branch to select the correct array.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .