Loading sprite atlases without much hassle
With our game project bloom, I wanted to make it easy for assets to be added into the editor. Our artist decided on a standard size for her sprite atlases, so all I needed was the asset file name to be postfixed with the number of sprites. Dropping these into the assets folder will automatically have it imported as a sprite sheet, with the sprites ready to use. Although they are 2D sprites, they are stored in an 3D texture as we use the z-layer as the sprite step. There’s also use of bindless handles here, but the implementation is the same without it.
//this function is used to load all the textures from the asset manager
void loadTextures()
{
unsigned int textureNumber = 0;
for (std::map>::iterator itr = AssetManager::getTextureMap().begin(); itr != AssetManager::getTextureMap().end(); ++itr)
{
int width, height, nrChannels;
unsigned char* data = stbi_load(itr->second.first.c_str(), &width, &height, &nrChannels, 0);
if (!data)
{
std::cout << "Failed to load texture" << std::endl;
}
std::string search("SpriteCount"); // Search for SpriteCount in image name
std::size_t found = itr->second.first.find(search); // Save position of where SpriteCount is found, End of string if can't find
// If the image is a spritesheet
if (found != std::string::npos)
{
int tileW = defaultTileW; // number of pixels in a row of 1 tile 512 is default
int tileH = defaultTileH; // number of pixels in a column of 1 tile
int channels = 4; // 4 for RGBA
int tilesX = width / tileW;
// this searches for the number of sprites in the file name and then converts it to an int
std::string numberOfSpritesString = itr->second.first.substr(found + search.size(), itr->second.first.find(".png") - (found + search.size()));
//only load once, so we use try catch to see if it's named properly
try
{
//if it's named properly, we can use it
int imageCount = stoi(numberOfSpritesString);
}
catch (const std::exception&)
{
//if it's not named properly, we use the default value and log an error
std::cout << "Error: " << itr->second.first << " is not named properly, using default value of 1" << std::endl;
int imageCount = 1;
}
//create the texture array and bind it, z-layer is the number of images in the spritesheet
glTextureStorage3D(arrayTextures[textureNumber], 1, GL_RGBA8, tileW, tileH, imageCount);
// pixel stores changes how the texture unpacking works, its so i can "cut" the big spritesheet into the correct tiles
//need to change back to 0 after all loading
glPixelStorei(GL_UNPACK_ROW_LENGTH, width);
for (int i = 0; i < imageCount; ++i)
{
int xOffSet = i % tilesX * tileW;
int yOffSet = height - ((i / tilesX + 1) * tileH);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, xOffSet);
glPixelStorei(GL_UNPACK_SKIP_ROWS, yOffSet);
//creating texture for each sprite
glTextureSubImage3D(arrayTextures[textureNumber], 0, 0, 0, i, tileW, tileH, 1, GL_RGBA, GL_UNSIGNED_BYTE, data);
}
//bindless texture handle we will use to access the texture
bindless = glGetTextureHandleARB(arrayTextures[textureNumber]);
//actually only need to make handle resident if you are going to use it, dont have to make all resident at the start
//!! need to make resident before using it, otherwise it will crash, and need to make non-resident before deleting the texture
//glMakeTextureHandleResidentARB(bindless);
bindlessTextures.push_back(bindless);
glBindTexture(GL_TEXTURE_2D_ARRAY, arrayTexture);
//bindlessTextureBuffer is a buffer that holds all the bindless texture handles
//there's an optmiization here, we only need to update the new part of the buffer, but for simplicity we update the whole buffer
glNamedBufferSubData(bindlessTextureBuffer, 0, sizeof(GLuint64) * maxTextures, bindlessTextures.data());
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
AssetManager::addSpritesheetMaxSteps(textureNumber, imageCount);
}
//if it's not a spritesheet, we just load it as a normal texture
else
{
glTextureStorage3D(arrayTextures[textureNumber], 1, GL_RGBA8, width, height, 1);
glTextureSubImage3D(arrayTextures[textureNumber], 0, 0, 0, 0, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, data);
bindless = glGetTextureHandleARB(arrayTextures[textureNumber]);
//glMakeTextureHandleResidentARB(bindless);
bindlessTextures.push_back(bindless);
glNamedBufferSubData(bindlessTextureBuffer, 0, sizeof(GLuint64) * maxTextures, bindlessTextures.data());
}
//create a texture view for the texture, so we can use it in imgui
GLuint newView;
glGenTextures(1, &newView);
glTextureView(newView, GL_TEXTURE_2D, arrayTextures[textureNumber], GL_RGBA8, 0, 1, 0, 1);
AssetManager::getTextureViews()[textureNumber] = newView;
stbi_image_free(data);
++textureNumber;
}
}