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;
	}
}	



Previous
Previous

Bitfield to compact neighbor information in A* grids.

Next
Next

Indirect Instanced draw calls